Support more keys accessibility mode

Bug: 12491371
Change-Id: Ib1fc8affbccfbaca3424ecdc2812f47047106aa2
main
Tadashi G. Takaoka 2014-05-26 11:09:07 +09:00
parent 1a0cd0869d
commit 62316d7e82
9 changed files with 367 additions and 22 deletions

View File

@ -48,6 +48,7 @@
<integer name="config_max_longpress_timeout">700</integer>
<integer name="config_min_longpress_timeout">100</integer>
<integer name="config_longpress_timeout_step">10</integer>
<integer name="config_accessibility_long_press_key_timeout">1500</integer>
<integer name="config_max_more_keys_column">5</integer>
<integer name="config_more_keys_keyboard_fadein_anim_time">0</integer>
<integer name="config_more_keys_keyboard_fadeout_anim_time">100</integer>

View File

@ -139,4 +139,9 @@
<string name="spoken_symbol_unknown">Unknown symbol</string>
<!-- Spoken description for unknown emoji code point. -->
<string name="spoken_emoji_unknown">Unknown emoji</string>
<!-- Spoken descriptions when opening a more keys keyboard that has alternative characters. -->
<string name="spoken_open_more_keys_keyboard">Alternative characters are available</string>
<!-- Spoken descriptions when closing a more keys keyboard that has alternative characters. -->
<string name="spoken_close_more_keys_keyboard">Alternative characters are dismissed</string>
</resources>

View File

@ -0,0 +1,67 @@
/*
* Copyright (C) 2014 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.content.Context;
import android.os.Handler;
import android.os.Message;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.latin.R;
// Handling long press timer to show a more keys keyboard.
final class AccessibilityLongPressTimer extends Handler {
public interface LongPressTimerCallback {
public void onLongPressed(Key key);
}
private static final int MSG_LONG_PRESS = 1;
private final LongPressTimerCallback mCallback;
private final long mConfigAccessibilityLongPressTimeout;
public AccessibilityLongPressTimer(final LongPressTimerCallback callback,
final Context context) {
super();
mCallback = callback;
mConfigAccessibilityLongPressTimeout = context.getResources().getInteger(
R.integer.config_accessibility_long_press_key_timeout);
}
@Override
public void handleMessage(final Message msg) {
switch (msg.what) {
case MSG_LONG_PRESS:
cancelLongPress();
mCallback.onLongPressed((Key)msg.obj);
return;
default:
super.handleMessage(msg);
return;
}
}
public void startLongPress(final Key key) {
cancelLongPress();
final Message longPressMessage = obtainMessage(MSG_LONG_PRESS, key);
sendMessageDelayed(longPressMessage, mConfigAccessibilityLongPressTimeout);
}
public void cancelLongPress() {
removeMessages(MSG_LONG_PRESS);
}
}

View File

@ -33,14 +33,29 @@ import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardView;
import com.android.inputmethod.keyboard.PointerTracker;
/**
* This class represents a delegate that can be registered in a class that extends
* {@link KeyboardView} to enhance accessibility support via composition rather via inheritance.
*
* To implement accessibility mode, the target keyboard view has to:<p>
* - Call {@link #setKeyboard(Keyboard)} when a new keyboard is set to the keyboard view.
* - Dispatch a hover event by calling {@link #onHoverEnter(MotionEvent)}.
*
* @param <KV> The keyboard view class type.
*/
public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
extends AccessibilityDelegateCompat {
private static final String TAG = KeyboardAccessibilityDelegate.class.getSimpleName();
protected static final boolean DEBUG_HOVER = false;
protected final KV mKeyboardView;
protected final KeyDetector mKeyDetector;
private Keyboard mKeyboard;
private KeyboardAccessibilityNodeProvider mAccessibilityNodeProvider;
private Key mLastHoverKey;
public static final int HOVER_EVENT_POINTER_ID = 0;
public KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector) {
super();
mKeyboardView = keyboardView;
@ -180,8 +195,11 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
*/
protected void onHoverEnter(final MotionEvent event) {
final Key key = getHoverKeyOf(event);
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnter: key=" + key);
}
if (key != null) {
onHoverEnterKey(key);
onHoverEnterTo(key);
}
setLastHoverKey(key);
}
@ -196,14 +214,14 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
final Key key = getHoverKeyOf(event);
if (key != lastKey) {
if (lastKey != null) {
onHoverExitKey(lastKey);
onHoverExitFrom(lastKey);
}
if (key != null) {
onHoverEnterKey(key);
onHoverEnterTo(key);
}
}
if (key != null) {
onHoverMoveKey(key);
onHoverMoveWithin(key);
}
setLastHoverKey(key);
}
@ -215,15 +233,18 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
*/
protected void onHoverExit(final MotionEvent event) {
final Key lastKey = getLastHoverKey();
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
}
if (lastKey != null) {
onHoverExitKey(lastKey);
onHoverExitFrom(lastKey);
}
final Key key = getHoverKeyOf(event);
// Make sure we're not getting an EXIT event because the user slid
// off the keyboard area, then force a key press.
if (key != null) {
onRegisterHoverKey(key, event);
onHoverExitKey(key);
onHoverExitFrom(key);
}
setLastHoverKey(null);
}
@ -235,6 +256,9 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
* @param event A hover exit event that triggers key registering.
*/
protected void onRegisterHoverKey(final Key key, final MotionEvent event) {
if (DEBUG_HOVER) {
Log.d(TAG, "onRegisterHoverKey: key=" + key);
}
simulateTouchEvent(MotionEvent.ACTION_DOWN, event);
simulateTouchEvent(MotionEvent.ACTION_UP, event);
}
@ -274,7 +298,10 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
*
* @param key The currently hovered key.
*/
protected void onHoverEnterKey(final Key key) {
protected void onHoverEnterTo(final Key key) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnterTo: key=" + key);
}
key.onPressed();
mKeyboardView.invalidateKey(key);
final KeyboardAccessibilityNodeProvider provider = getAccessibilityNodeProvider();
@ -287,14 +314,17 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
*
* @param key The currently hovered key.
*/
protected void onHoverMoveKey(final Key key) { }
protected void onHoverMoveWithin(final Key key) { }
/**
* Handles a hover exit event on a key.
*
* @param key The currently hovered key.
*/
protected void onHoverExitKey(final Key key) {
protected void onHoverExitFrom(final Key key) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExitFrom: key=" + key);
}
key.onReleased();
mKeyboardView.invalidateKey(key);
final KeyboardAccessibilityNodeProvider provider = getAccessibilityNodeProvider();

View File

@ -17,17 +17,29 @@
package com.android.inputmethod.accessibility;
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.MotionEvent;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardId;
import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.keyboard.PointerTracker;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
/**
* This class represents a delegate that can be registered in {@link MainKeyboardView} to enhance
* accessibility support via composition rather via inheritance.
*/
public final class MainKeyboardAccessibilityDelegate
extends KeyboardAccessibilityDelegate<MainKeyboardView> {
extends KeyboardAccessibilityDelegate<MainKeyboardView>
implements AccessibilityLongPressTimer.LongPressTimerCallback {
private static final String TAG = MainKeyboardAccessibilityDelegate.class.getSimpleName();
/** Map of keyboard modes to resource IDs. */
private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray();
@ -46,10 +58,15 @@ public final class MainKeyboardAccessibilityDelegate
/** The most recently set keyboard mode. */
private int mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
private static final int KEYBOARD_IS_HIDDEN = -1;
private boolean mShouldIgnoreOnRegisterHoverKey;
private final AccessibilityLongPressTimer mAccessibilityLongPressTimer;
public MainKeyboardAccessibilityDelegate(final MainKeyboardView mainKeyboardView,
final KeyDetector keyDetector) {
super(mainKeyboardView, keyDetector);
mAccessibilityLongPressTimer = new AccessibilityLongPressTimer(
this /* callback */, mainKeyboardView.getContext());
}
/**
@ -172,4 +189,63 @@ public final class MainKeyboardAccessibilityDelegate
private void announceKeyboardHidden() {
sendWindowStateChanged(R.string.announce_keyboard_hidden);
}
@Override
protected void onRegisterHoverKey(final Key key, final MotionEvent event) {
if (DEBUG_HOVER) {
Log.d(TAG, "onRegisterHoverKey: key=" + key + " ignore="
+ mShouldIgnoreOnRegisterHoverKey);
}
if (!mShouldIgnoreOnRegisterHoverKey) {
super.onRegisterHoverKey(key, event);
}
mShouldIgnoreOnRegisterHoverKey = false;
}
@Override
protected void onHoverEnterTo(final Key key) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnterTo: key=" + key);
}
mAccessibilityLongPressTimer.cancelLongPress();
super.onHoverEnterTo(key);
if (key.isLongPressEnabled()) {
mAccessibilityLongPressTimer.startLongPress(key);
}
}
protected void onHoverExitFrom(final Key key) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExitFrom: key=" + key);
}
mAccessibilityLongPressTimer.cancelLongPress();
super.onHoverExitFrom(key);
}
@Override
public void onLongPressed(final Key key) {
if (DEBUG_HOVER) {
Log.d(TAG, "onLongPressed: key=" + key);
}
final PointerTracker tracker = PointerTracker.getPointerTracker(HOVER_EVENT_POINTER_ID);
final long eventTime = SystemClock.uptimeMillis();
final int x = key.getHitBox().centerX();
final int y = key.getHitBox().centerY();
final MotionEvent downEvent = MotionEvent.obtain(
eventTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0 /* metaState */);
// Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
tracker.processMotionEvent(downEvent, mKeyDetector);
// The above fake down event triggers an unnecessary long press timer that should be
// canceled.
tracker.cancelLongPressTimer();
downEvent.recycle();
// Invoke {@link MainKeyboardView#onLongPress(PointerTracker)} as if a long press timeout
// has passed.
mKeyboardView.onLongPress(tracker);
// If {@link Key#hasNoPanelAutoMoreKeys()} is true (such as "0 +" key on the phone layout)
// or a key invokes IME switcher dialog, we should just ignore the next
// {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
// {@link PointerTracker} is in operation or not.
mShouldIgnoreOnRegisterHoverKey = !tracker.isInOperation();
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright (C) 2014 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.util.Log;
import android.view.MotionEvent;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.MoreKeysKeyboardView;
import com.android.inputmethod.latin.Constants;
/**
* This class represents a delegate that can be registered in {@link MoreKeysKeyboardView} to
* enhance accessibility support via composition rather via inheritance.
*/
public class MoreKeysKeyboardAccessibilityDelegate
extends KeyboardAccessibilityDelegate<MoreKeysKeyboardView> {
private static final String TAG = MoreKeysKeyboardAccessibilityDelegate.class.getSimpleName();
private final Rect mMoreKeysKeyboardValidBounds = new Rect();
private static final int CLOSING_INSET_IN_PIXEL = 1;
private int mOpenAnnounceResId;
private int mCloseAnnounceResId;
public MoreKeysKeyboardAccessibilityDelegate(final MoreKeysKeyboardView moreKeysKeyboardView,
final KeyDetector keyDetector) {
super(moreKeysKeyboardView, keyDetector);
}
public void setOpenAnnounce(final int resId) {
mOpenAnnounceResId = resId;
}
public void setCloseAnnounce(final int resId) {
mCloseAnnounceResId = resId;
}
public void onShowMoreKeysKeyboard() {
sendWindowStateChanged(mOpenAnnounceResId);
}
@Override
protected void onHoverEnter(final MotionEvent event) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnter: key=" + getHoverKeyOf(event));
}
super.onHoverEnter(event);
final int actionIndex = event.getActionIndex();
final int x = (int)event.getX(actionIndex);
final int y = (int)event.getY(actionIndex);
final int pointerId = event.getPointerId(actionIndex);
final long eventTime = event.getEventTime();
mKeyboardView.onDownEvent(x, y, pointerId, eventTime);
}
@Override
protected void onHoverMove(final MotionEvent event) {
super.onHoverMove(event);
final int actionIndex = event.getActionIndex();
final int x = (int)event.getX(actionIndex);
final int y = (int)event.getY(actionIndex);
final int pointerId = event.getPointerId(actionIndex);
final long eventTime = event.getEventTime();
mKeyboardView.onMoveEvent(x, y, pointerId, eventTime);
}
@Override
protected void onHoverExit(final MotionEvent event) {
final Key lastKey = getLastHoverKey();
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
}
if (lastKey != null) {
super.onHoverExitFrom(lastKey);
}
setLastHoverKey(null);
final int actionIndex = event.getActionIndex();
final int x = (int)event.getX(actionIndex);
final int y = (int)event.getY(actionIndex);
final int pointerId = event.getPointerId(actionIndex);
final long eventTime = event.getEventTime();
// A hover exit event at one pixel width or height area on the edges of more keys keyboard
// are treated as closing.
mMoreKeysKeyboardValidBounds.set(0, 0, mKeyboardView.getWidth(), mKeyboardView.getHeight());
mMoreKeysKeyboardValidBounds.inset(CLOSING_INSET_IN_PIXEL, CLOSING_INSET_IN_PIXEL);
if (mMoreKeysKeyboardValidBounds.contains(x, y)) {
// Invoke {@link MoreKeysKeyboardView#onUpEvent(int,int,int,long)} as if this hover
// exit event selects a key.
mKeyboardView.onUpEvent(x, y, pointerId, eventTime);
mKeyboardView.dismissMoreKeysPanel();
return;
}
// Close the more keys keyboard.
mKeyboardView.onMoveEvent(
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, pointerId, eventTime);
sendWindowStateChanged(mCloseAnnounceResId);
}
}

View File

@ -17,12 +17,13 @@
package com.android.inputmethod.keyboard;
import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.android.inputmethod.accessibility.AccessibilityUtils;
import com.android.inputmethod.accessibility.MoreKeysKeyboardAccessibilityDelegate;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.utils.CoordinateUtils;
@ -34,7 +35,7 @@ import com.android.inputmethod.latin.utils.CoordinateUtils;
public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel {
private final int[] mCoordinates = CoordinateUtils.newInstance();
protected final KeyDetector mKeyDetector;
protected KeyDetector mKeyDetector;
private Controller mController = EMPTY_CONTROLLER;
protected KeyboardActionListener mListener;
private int mOriginX;
@ -43,6 +44,8 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
private int mActivePointerId;
private MoreKeysKeyboardAccessibilityDelegate mAccessibilityDelegate;
public MoreKeysKeyboardView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.moreKeysKeyboardViewStyle);
}
@ -50,10 +53,8 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
public MoreKeysKeyboardView(final Context context, final AttributeSet attrs,
final int defStyle) {
super(context, attrs, defStyle);
final Resources res = context.getResources();
mKeyDetector = new MoreKeysDetector(
res.getDimension(R.dimen.config_more_keys_keyboard_slide_allowance));
mKeyDetector = new MoreKeysDetector(getResources().getDimension(
R.dimen.config_more_keys_keyboard_slide_allowance));
}
@Override
@ -71,8 +72,23 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
@Override
public void setKeyboard(final Keyboard keyboard) {
super.setKeyboard(keyboard);
mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
-getPaddingTop() + getVerticalCorrection());
if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
// With accessibility mode on, any hover event outside {@link MoreKeysKeyboardView} is
// discarded at {@link InputView#dispatchHoverEvent(MotionEvent)}. Because only a hover
// event that is on this view is dispatched by the platform, we should use a
// {@link KeyDetector} that has no sliding allowance and no hysteresis.
mKeyDetector = new KeyDetector();
mAccessibilityDelegate = new MoreKeysKeyboardAccessibilityDelegate(this, mKeyDetector);
mAccessibilityDelegate.setOpenAnnounce(R.string.spoken_open_more_keys_keyboard);
mAccessibilityDelegate.setCloseAnnounce(R.string.spoken_close_more_keys_keyboard);
mAccessibilityDelegate.setKeyboard(keyboard);
} else {
mKeyDetector = new MoreKeysDetector(getResources().getDimension(
R.dimen.config_more_keys_keyboard_slide_allowance));
mAccessibilityDelegate = null;
}
mKeyDetector.setKeyboard(
keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
}
@Override
@ -98,6 +114,10 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
mOriginX = x + container.getPaddingLeft();
mOriginY = y + container.getPaddingTop();
controller.onShowMoreKeysPanel(this);
final MoreKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
if (accessibilityDelegate != null) {
accessibilityDelegate.onShowMoreKeysKeyboard();
}
}
/**
@ -228,6 +248,18 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean onHoverEvent(final MotionEvent event) {
final MoreKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
if (accessibilityDelegate != null) {
return accessibilityDelegate.onHoverEvent(event);
}
return super.onHoverEvent(event);
}
private View getContainerView() {
return (View)getParent();
}

View File

@ -1078,6 +1078,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
mIsTrackingForActionDisabled = true;
}
public boolean isInOperation() {
return !mIsTrackingForActionDisabled;
}
public void cancelLongPressTimer() {
sTimerProxy.cancelLongPressTimerOf(this);
}
public void onLongPressed() {
resetKeySelectionByDraggingFinger();
cancelTrackingForAction();

View File

@ -23,12 +23,14 @@ import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import com.android.inputmethod.accessibility.AccessibilityUtils;
import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.latin.suggestions.MoreSuggestionsView;
import com.android.inputmethod.latin.suggestions.SuggestionStripView;
public final class InputView extends LinearLayout {
private final Rect mInputViewRect = new Rect();
private MainKeyboardView mMainKeyboardView;
private KeyboardTopPaddingForwarder mKeyboardTopPaddingForwarder;
private MoreSuggestionsViewCanceler mMoreSuggestionsViewCanceler;
private MotionEventForwarder<?, ?> mActiveForwarder;
@ -41,18 +43,28 @@ public final class InputView extends LinearLayout {
protected void onFinishInflate() {
final SuggestionStripView suggestionStripView =
(SuggestionStripView)findViewById(R.id.suggestion_strip_view);
final MainKeyboardView mainKeyboardView =
(MainKeyboardView)findViewById(R.id.keyboard_view);
mMainKeyboardView = (MainKeyboardView)findViewById(R.id.keyboard_view);
mKeyboardTopPaddingForwarder = new KeyboardTopPaddingForwarder(
mainKeyboardView, suggestionStripView);
mMainKeyboardView, suggestionStripView);
mMoreSuggestionsViewCanceler = new MoreSuggestionsViewCanceler(
mainKeyboardView, suggestionStripView);
mMainKeyboardView, suggestionStripView);
}
public void setKeyboardTopPadding(final int keyboardTopPadding) {
mKeyboardTopPaddingForwarder.setKeyboardTopPadding(keyboardTopPadding);
}
@Override
protected boolean dispatchHoverEvent(final MotionEvent event) {
if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()
&& mMainKeyboardView.isShowingMoreKeysPanel()) {
// With accessibility mode on, discard hover events while a more keys keyboard is shown.
// The {@link MoreKeysKeyboard} receives hover events directly from the platform.
return true;
}
return super.dispatchHoverEvent(event);
}
@Override
public boolean onInterceptTouchEvent(final MotionEvent me) {
final Rect rect = mInputViewRect;