Make LatinIME keys accessibility focusable, clickable.

Also fix speech for labeled keys.

Bug: 6498563
Change-Id: I094d4db0e57fa373759a63eb3354b1ab3ab0f525
This commit is contained in:
alanv 2012-05-15 15:11:12 -07:00
parent b4b3e80f11
commit f2eba97cc0
3 changed files with 142 additions and 26 deletions

View file

@ -18,13 +18,18 @@ package com.android.inputmethod.accessibility;
import android.graphics.Rect; import android.graphics.Rect;
import android.inputmethodservice.InputMethodService; import android.inputmethodservice.InputMethodService;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v4.view.ViewCompat; 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.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewParent;
import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
@ -45,6 +50,7 @@ import com.android.inputmethod.keyboard.KeyboardView;
*/ */
public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat { public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat {
private static final String TAG = AccessibilityEntityProvider.class.getSimpleName(); private static final String TAG = AccessibilityEntityProvider.class.getSimpleName();
private static final int UNDEFINED = Integer.MIN_VALUE;
private final KeyboardView mKeyboardView; private final KeyboardView mKeyboardView;
private final InputMethodService mInputMethodService; private final InputMethodService mInputMethodService;
@ -60,6 +66,9 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat
/** The parent view's cached on-screen location. */ /** The parent view's cached on-screen location. */
private final int[] mParentLocation = new int[2]; private final int[] mParentLocation = new int[2];
/** The virtual view identifier for the focused node. */
private int mAccessibilityFocusedView = UNDEFINED;
public AccessibilityEntityProvider(KeyboardView keyboardView, InputMethodService inputMethod) { public AccessibilityEntityProvider(KeyboardView keyboardView, InputMethodService inputMethod) {
mKeyboardView = keyboardView; mKeyboardView = keyboardView;
mInputMethodService = inputMethod; mInputMethodService = inputMethod;
@ -124,7 +133,9 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat
public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) { public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
AccessibilityNodeInfoCompat info = null; AccessibilityNodeInfoCompat info = null;
if (virtualViewId == View.NO_ID) { if (virtualViewId == UNDEFINED) {
return null;
} else if (virtualViewId == View.NO_ID) {
// We are requested to create an AccessibilityNodeInfo describing // We are requested to create an AccessibilityNodeInfo describing
// this View, i.e. the root of the virtual sub-tree. // this View, i.e. the root of the virtual sub-tree.
info = AccessibilityNodeInfoCompat.obtain(mKeyboardView); info = AccessibilityNodeInfoCompat.obtain(mKeyboardView);
@ -166,11 +177,114 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat
info.setSource(mKeyboardView, virtualViewId); info.setSource(mKeyboardView, virtualViewId);
info.setBoundsInScreen(boundsInScreen); info.setBoundsInScreen(boundsInScreen);
info.setEnabled(true); info.setEnabled(true);
info.setClickable(true);
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
if (mAccessibilityFocusedView == virtualViewId) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
} else {
info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
}
} }
return info; 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(Key key) {
final int x = key.mX + (key.mWidth / 2);
final int y = key.mY + (key.mHeight / 2);
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);
}
@Override
public boolean performAction(int virtualViewId, int action, 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(Key key, int action, Bundle arguments) {
final int virtualViewId = generateVirtualViewIdForKey(key);
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_CLICK:
simulateKeyPress(key);
return true;
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;
}
return false;
}
@Override
public AccessibilityNodeInfoCompat findAccessibilityFocus(int virtualViewId) {
return createAccessibilityNodeInfo(mAccessibilityFocusedView);
}
@Override
public AccessibilityNodeInfoCompat accessibilityFocusSearch(int direction, int virtualViewId) {
// Focus search is not currently supported for IMEs.
return null;
}
/**
* 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(Key key, int eventType) {
final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
final ViewParent parent = mKeyboardView.getParent();
if (parent == null) {
return;
}
parent.requestSendAccessibilityEvent(mKeyboardView, event);
}
/** /**
* Returns the context-specific description for a {@link Key}. * Returns the context-specific description for a {@link Key}.
* *

View file

@ -21,10 +21,10 @@ import android.inputmethodservice.InputMethodService;
import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewConfiguration; import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.Keyboard;
@ -91,13 +91,7 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
*/ */
@Override @Override
public AccessibilityEntityProvider getAccessibilityNodeProvider(View host) { public AccessibilityEntityProvider getAccessibilityNodeProvider(View host) {
// Instantiate the provide only when requested. Since the system return getAccessibilityNodeProvider();
// will call this method multiple times it is a good practice to
// cache the provider instance.
if (mAccessibilityNodeProvider == null) {
mAccessibilityNodeProvider = new AccessibilityEntityProvider(mView, mInputMethod);
}
return mAccessibilityNodeProvider;
} }
/** /**
@ -120,7 +114,7 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
// Make sure we're not getting an EXIT event because the user slid // Make sure we're not getting an EXIT event because the user slid
// off the keyboard area, then force a key press. // off the keyboard area, then force a key press.
if (pointInView(x, y)) { if (pointInView(x, y)) {
tracker.onRegisterKey(key); getAccessibilityNodeProvider().simulateKeyPress(key);
} }
//$FALL-THROUGH$ //$FALL-THROUGH$
case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_ENTER:
@ -136,6 +130,19 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
return false; return false;
} }
/**
* @return A lazily-instantiated node provider for this view proxy.
*/
private AccessibilityEntityProvider getAccessibilityNodeProvider() {
// Instantiate the provide only when requested. Since the system
// will call this method multiple times it is a good practice to
// cache the provider instance.
if (mAccessibilityNodeProvider == null) {
mAccessibilityNodeProvider = new AccessibilityEntityProvider(mView, mInputMethod);
}
return mAccessibilityNodeProvider;
}
/** /**
* Utility method to determine whether the given point, in local * Utility method to determine whether the given point, in local
* coordinates, is inside the view, where the area of the view is contracted * coordinates, is inside the view, where the area of the view is contracted
@ -191,32 +198,24 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
return false; return false;
} }
final AccessibilityEntityProvider provider = getAccessibilityNodeProvider();
switch (event.getAction()) { switch (event.getAction()) {
case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_ENTER:
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER); provider.sendAccessibilityEventForKey(
key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
provider.performActionForKey(
key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
break; break;
case MotionEvent.ACTION_HOVER_EXIT: case MotionEvent.ACTION_HOVER_EXIT:
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT); provider.sendAccessibilityEventForKey(
key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
break; break;
} }
return true; return true;
} }
/**
* Populates and sends an {@link AccessibilityEvent} for the specified key.
*
* @param key The key to send an event for.
* @param eventType The type of event to send.
*/
private void sendAccessibilityEventForKey(Key key, int eventType) {
final AccessibilityEntityProvider nodeProvider = getAccessibilityNodeProvider(null);
final AccessibilityEvent event = nodeProvider.createAccessibilityEvent(key, eventType);
// Propagates the event up the view hierarchy.
mView.getParent().requestSendAccessibilityEvent(mView, event);
}
/** /**
* Notifies the user of changes in the keyboard shift state. * Notifies the user of changes in the keyboard shift state.
*/ */

View file

@ -111,6 +111,9 @@ public class KeyCodeDescriptionMapper {
if (mKeyLabelMap.containsKey(label)) { if (mKeyLabelMap.containsKey(label)) {
return context.getString(mKeyLabelMap.get(label)); return context.getString(mKeyLabelMap.get(label));
} }
// Otherwise, return the label.
return key.mLabel;
} }
// Just attempt to speak the description. // Just attempt to speak the description.