Added support for speaking keys through accessibility service.

Issue: 3366636

Change-Id: I70a2ebb4420782ba4d7d874ffc2259bb1b80446f
main
Alan Viverette 2011-01-14 10:49:21 -08:00
parent ededda059c
commit dcade18113
8 changed files with 350 additions and 13 deletions

View File

@ -28,4 +28,24 @@
<integer name="key_delete">-5</integer>
<integer name="key_settings">-100</integer>
<integer name="key_voice">-102</integer>
<!-- Array used for mapping key codes to description strings. -->
<array name="key_descriptions">
<item>@integer/key_tab</item>
<item>@string/description_tab_key</item>
<item>@integer/key_return</item>
<item>@string/description_return_key</item>
<item>@integer/key_space</item>
<item>@string/description_space_key</item>
<item>@integer/key_shift</item>
<item>@string/description_shift_key</item>
<item>@integer/key_switch_alpha_symbol</item>
<item>@string/description_switch_alpha_symbol_key</item>
<item>@integer/key_delete</item>
<item>@string/description_delete_key</item>
<item>@integer/key_settings</item>
<item>@string/description_settings_key</item>
<item>@integer/key_voice</item>
<item>@string/description_voice_key</item>
</array>
</resources>

View File

@ -102,6 +102,31 @@
<!-- Label for "Wait" key of phone number keyboard. Must be short to fit on key! [CHAR LIMIT=5]-->
<string name="label_wait_key">Wait</string>
<!-- Spoken text description for delete key. -->
<string name="description_delete_key">Delete</string>
<!-- Spoken text description for return key. -->
<string name="description_return_key">Return</string>
<!-- Spoken text description for settings key. -->
<string name="description_settings_key">Settings</string>
<!-- Spoken text description for shift key. -->
<string name="description_shift_key">Shift</string>
<!-- Spoken text description for space key. -->
<string name="description_space_key">Space</string>
<!-- Spoken text description for symbols key. -->
<string name="description_switch_alpha_symbol_key">Symbols</string>
<!-- Spoken text description for tab key. -->
<string name="description_tab_key">Tab</string>
<!-- Spoken text description for voice input key. -->
<string name="description_voice_key">Voice Input</string>
<!-- Spoken text description for symbols mode on. -->
<string name="description_symbols_on">Symbols on</string>
<!-- Spoken text description for symbols mode off. -->
<string name="description_symbols_off">Symbols off</string>
<!-- Spoken text description for shift mode on. -->
<string name="description_shift_on">Shift on</string>
<!-- Spoken text description for shift mode off. -->
<string name="description_shift_off">Shift off</string>
<!-- Voice related labels -->
<!-- Title of the warning dialog that shows when a user initiates voice input for

View File

@ -336,7 +336,8 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha
// state when shift key is pressed to go to normal mode.
// On the other hand, on distinct multi touch panel device, turning off the shift locked
// state with shift key pressing is handled by onReleaseShift().
if (!hasDistinctMultitouch() && !shifted && latinKeyboard.isShiftLocked()) {
if ((!hasDistinctMultitouch() || isAccessibilityEnabled())
&& !shifted && latinKeyboard.isShiftLocked()) {
latinKeyboard.setShiftLocked(false);
}
if (latinKeyboard.setShifted(shifted)) {
@ -434,6 +435,9 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha
public void onPressShift() {
if (!isKeyboardAvailable())
return;
// If accessibility is enabled, disable momentary shift lock.
if (isAccessibilityEnabled())
return;
ShiftKeyState shiftKeyState = mShiftKeyState;
if (DEBUG_STATE)
Log.d(TAG, "onPressShift:"
@ -469,6 +473,9 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha
public void onReleaseShift() {
if (!isKeyboardAvailable())
return;
// If accessibility is enabled, disable momentary shift lock.
if (isAccessibilityEnabled())
return;
ShiftKeyState shiftKeyState = mShiftKeyState;
if (DEBUG_STATE)
Log.d(TAG, "onReleaseShift:"
@ -494,6 +501,9 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha
}
public void onPressSymbol() {
// If accessibility is enabled, disable momentary symbol lock.
if (isAccessibilityEnabled())
return;
if (DEBUG_STATE)
Log.d(TAG, "onPressSymbol:"
+ " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
@ -504,6 +514,9 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha
}
public void onReleaseSymbol() {
// If accessibility is enabled, disable momentary symbol lock.
if (isAccessibilityEnabled())
return;
if (DEBUG_STATE)
Log.d(TAG, "onReleaseSymbol:"
+ " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
@ -516,6 +529,9 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha
}
public void onOtherKeyPressed() {
// If accessibility is enabled, disable momentary mode locking.
if (isAccessibilityEnabled())
return;
if (DEBUG_STATE)
Log.d(TAG, "onOtherKeyPressed:"
+ " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
@ -574,6 +590,10 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha
}
}
public boolean isAccessibilityEnabled() {
return mInputView != null && mInputView.isAccessibilityEnabled();
}
public boolean hasDistinctMultitouch() {
return mInputView != null && mInputView.hasDistinctMultitouch();
}

View File

@ -37,6 +37,7 @@ import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
@ -146,6 +147,9 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
private final boolean mHasDistinctMultitouch;
private int mOldPointerCount = 1;
// Accessibility
private boolean mIsAccessibilityEnabled;
protected KeyDetector mKeyDetector = new ProximityKeyDetector();
// Swipe gesture detector
@ -523,7 +527,7 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
}
/**
* Return whether the device has distinct multi-touch panel.
* Returns whether the device has distinct multi-touch panel.
* @return true if the device has distinct multi-touch panel.
*/
@Override
@ -531,6 +535,28 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
return mHasDistinctMultitouch;
}
/**
* Enables or disables accessibility.
* @param accessibilityEnabled whether or not to enable accessibility
*/
public void setAccessibilityEnabled(boolean accessibilityEnabled) {
mIsAccessibilityEnabled = accessibilityEnabled;
// Propagate this change to all existing pointer trackers.
for (PointerTracker tracker : mPointerTrackers) {
tracker.setAccessibilityEnabled(accessibilityEnabled);
}
}
/**
* Returns whether the device has accessibility enabled.
* @return true if the device has accessibility enabled.
*/
@Override
public boolean isAccessibilityEnabled() {
return mIsAccessibilityEnabled;
}
/**
* Enables or disables the key feedback popup. This is a popup that shows a magnified
* version of the depressed key. By default the preview is enabled.
@ -1210,15 +1236,18 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
// TODO: cleanup this code into a multi-touch to single-touch event converter class?
// If the device does not have distinct multi-touch support panel, ignore all multi-touch
// events except a transition from/to single-touch.
if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
if ((!mHasDistinctMultitouch || mIsAccessibilityEnabled)
&& pointerCount > 1 && oldPointerCount > 1) {
return true;
}
// Track the last few movements to look for spurious swipes.
mSwipeTracker.addMovement(me);
// Gesture detector must be enabled only when mini-keyboard is not on the screen.
if (mMiniKeyboardView == null
// Gesture detector must be enabled only when mini-keyboard is not on the screen and
// accessibility is not enabled.
// TODO: Reconcile gesture detection and accessibility features.
if (mMiniKeyboardView == null && !mIsAccessibilityEnabled
&& mGestureDetector != null && mGestureDetector.onTouchEvent(me)) {
dismissKeyPreview();
mHandler.cancelKeyTimers();
@ -1263,7 +1292,7 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
// TODO: cleanup this code into a multi-touch to single-touch event converter class?
// Translate mutli-touch event to single-touch events on the device that has no distinct
// multi-touch panel.
if (!mHasDistinctMultitouch) {
if (!mHasDistinctMultitouch || mIsAccessibilityEnabled) {
// Use only main (id=0) pointer tracker.
PointerTracker tracker = getPointerTracker(0);
if (pointerCount == 1 && oldPointerCount == 2) {

View File

@ -145,6 +145,10 @@ public class LatinKeyboardView extends KeyboardView {
// If device has distinct multi touch panel, there is no need to check sudden jump.
if (hasDistinctMultitouch())
return false;
// If accessibiliy is enabled, stop looking for sudden jumps because it interferes
// with touch exploration of the keyboard.
if (isAccessibilityEnabled())
return false;
final int action = me.getAction();
final int x = (int) me.getX();
final int y = (int) me.getY();

View File

@ -38,6 +38,7 @@ public class PointerTracker {
public void invalidateKey(Key key);
public void showPreview(int keyIndex, PointerTracker tracker);
public boolean hasDistinctMultitouch();
public boolean isAccessibilityEnabled();
}
public final int mPointerId;
@ -68,6 +69,9 @@ public class PointerTracker {
private final PointerTrackerKeyState mKeyState;
// true if accessibility is enabled in the parent keyboard
private boolean mIsAccessibilityEnabled;
// true if keyboard layout has been changed.
private boolean mKeyboardLayoutHasBeenChanged;
@ -112,6 +116,7 @@ public class PointerTracker {
mKeyDetector = keyDetector;
mKeyboardSwitcher = KeyboardSwitcher.getInstance();
mKeyState = new PointerTrackerKeyState(keyDetector);
mIsAccessibilityEnabled = proxy.isAccessibilityEnabled();
mHasDistinctMultitouch = proxy.hasDistinctMultitouch();
mConfigSlidingKeyInputEnabled = res.getBoolean(R.bool.config_sliding_key_input_enabled);
mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start);
@ -128,6 +133,10 @@ public class PointerTracker {
mListener = listener;
}
public void setAccessibilityEnabled(boolean accessibilityEnabled) {
mIsAccessibilityEnabled = accessibilityEnabled;
}
// Returns true if keyboard has been changed by this callback.
private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) {
if (DEBUG_LISTENER)
@ -312,9 +321,10 @@ public class PointerTracker {
private void onDownEventInternal(int x, int y, long eventTime) {
int keyIndex = mKeyState.onDownKey(x, y, eventTime);
// Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
// from modifier key, or 3) this pointer is on mini-keyboard.
// from modifier key, 3) this pointer is on mini-keyboard, or 4) accessibility is enabled.
mIsAllowedSlidingKeyInput = mConfigSlidingKeyInputEnabled || isModifierInternal(keyIndex)
|| mKeyDetector instanceof MiniKeyboardKeyDetector;
|| mKeyDetector instanceof MiniKeyboardKeyDetector
|| mIsAccessibilityEnabled;
mKeyboardLayoutHasBeenChanged = false;
mKeyAlreadyProcessed = false;
mIsRepeatableKey = false;
@ -327,7 +337,9 @@ public class PointerTracker {
keyIndex = mKeyState.onDownKey(x, y, eventTime);
}
if (isValidKeyIndex(keyIndex)) {
if (mKeys[keyIndex].mRepeatable) {
// Accessibility disables key repeat because users may need to pause on a key to hear
// its spoken description.
if (mKeys[keyIndex].mRepeatable && !mIsAccessibilityEnabled) {
repeatKey(keyIndex);
mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this);
mIsRepeatableKey = true;
@ -517,8 +529,9 @@ public class PointerTracker {
updateKeyGraphics(keyIndex);
// The modifier key, such as shift key, should not be shown as preview when multi-touch is
// supported. On the other hand, if multi-touch is not supported, the modifier key should
// be shown as preview.
if (mHasDistinctMultitouch && isModifier()) {
// be shown as preview. If accessibility is turned on, the modifier key should be shown as
// preview.
if (mHasDistinctMultitouch && isModifier() && !mIsAccessibilityEnabled) {
mProxy.showPreview(NOT_A_KEY, this);
} else {
mProxy.showPreview(keyIndex, this);
@ -526,6 +539,11 @@ public class PointerTracker {
}
private void startLongPressTimer(int keyIndex) {
// Accessibility disables long press because users are likely to need to pause on a key
// for an unspecified duration in order to hear the key's spoken description.
if (mIsAccessibilityEnabled) {
return;
}
Key key = getKey(keyIndex);
if (key.mCode == Keyboard.CODE_SHIFT) {
mHandler.startLongPressShiftTimer(mLongPressShiftKeyTimeout, keyIndex, this);

View File

@ -0,0 +1,211 @@
/*
* Copyright (C) 2011 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.latin;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import java.util.HashMap;
import java.util.Map;
/**
* Utility functions for accessibility support.
*/
public class AccessibilityUtils {
/** Shared singleton instance. */
private static final AccessibilityUtils sInstance = new AccessibilityUtils();
private /* final */ LatinIME mService;
private /* final */ AccessibilityManager mAccessibilityManager;
private /* final */ Map<Integer, CharSequence> mDescriptions;
/**
* Returns a shared instance of AccessibilityUtils.
*
* @return A shared instance of AccessibilityUtils.
*/
public static AccessibilityUtils getInstance() {
return sInstance;
}
/**
* Initializes (or re-initializes) the shared instance of AccessibilityUtils
* with the specified parent service and preferences.
*
* @param service The parent input method service.
* @param prefs The parent preferences.
*/
public static void init(LatinIME service, SharedPreferences prefs) {
sInstance.initialize(service, prefs);
}
private AccessibilityUtils() {
// This class is not publicly instantiable.
}
/**
* Initializes (or re-initializes) with the specified parent service and
* preferences.
*
* @param service The parent input method service.
* @param prefs The parent preferences.
*/
private void initialize(LatinIME service, SharedPreferences prefs) {
mService = service;
mAccessibilityManager = (AccessibilityManager) service.getSystemService(
Context.ACCESSIBILITY_SERVICE);
mDescriptions = null;
}
/**
* Returns true if accessibility is enabled.
*
* @return {@code true} if accessibility is enabled.
*/
public boolean isAccessibilityEnabled() {
return mAccessibilityManager.isEnabled();
}
/**
* Speaks a key's action after it has been released. Does not speak letter
* keys since typed keys are already spoken aloud by TalkBack.
* <p>
* No-op if accessibility is not enabled.
* </p>
*
* @param primaryCode The primary code of the released key.
* @param switcher The input method's {@link KeyboardSwitcher}.
*/
public void onRelease(int primaryCode, KeyboardSwitcher switcher) {
if (!isAccessibilityEnabled()) {
return;
}
int resId = -1;
switch (primaryCode) {
case Keyboard.CODE_SHIFT: {
if (switcher.isShiftedOrShiftLocked()) {
resId = R.string.description_shift_on;
} else {
resId = R.string.description_shift_off;
}
break;
}
case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: {
if (switcher.isAlphabetMode()) {
resId = R.string.description_symbols_off;
} else {
resId = R.string.description_symbols_on;
}
break;
}
}
if (resId >= 0) {
speakDescription(mService.getResources().getText(resId));
}
}
/**
* Speaks a key's description for accessibility. If a key has an explicit
* description defined in keycodes.xml, that will be used. Otherwise, if the
* key is a Unicode character, then its character will be used.
* <p>
* No-op if accessibility is not enabled.
* </p>
*
* @param primaryCode The primary code of the pressed key.
* @param switcher The input method's {@link KeyboardSwitcher}.
*/
public void onPress(int primaryCode, KeyboardSwitcher switcher) {
if (!isAccessibilityEnabled()) {
return;
}
// TODO Use the current keyboard state to read "Switch to symbols"
// instead of just "Symbols" (and similar for shift key).
CharSequence description = describeKey(primaryCode);
if (description == null && Character.isDefined((char) primaryCode)) {
description = Character.toString((char) primaryCode);
}
if (description != null) {
speakDescription(description);
}
}
/**
* Returns a text description for a given key code. If the key does not have
* an explicit description, returns <code>null</code>.
*
* @param keyCode An integer key code.
* @return A {@link CharSequence} describing the key or <code>null</code> if
* no description is available.
*/
private CharSequence describeKey(int keyCode) {
// If not loaded yet, load key descriptions from XML file.
if (mDescriptions == null) {
mDescriptions = loadDescriptions();
}
return mDescriptions.get(keyCode);
}
/**
* Loads key descriptions from resources.
*/
private Map<Integer, CharSequence> loadDescriptions() {
final Map<Integer, CharSequence> descriptions = new HashMap<Integer, CharSequence>();
final TypedArray array = mService.getResources().obtainTypedArray(R.array.key_descriptions);
// Key descriptions are stored as a key code followed by a string.
for (int i = 0; i < array.length() - 1; i += 2) {
int code = array.getInteger(i, 0);
CharSequence desc = array.getText(i + 1);
descriptions.put(code, desc);
}
array.recycle();
return descriptions;
}
/**
* Sends a character sequence to be read aloud.
*
* @param description The {@link CharSequence} to be read aloud.
*/
private void speakDescription(CharSequence description) {
// TODO We need to add an AccessibilityEvent type for IMEs.
final AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
event.setPackageName(mService.getPackageName());
event.setClassName(getClass().getName());
event.setAddedCount(description.length());
event.getText().add(description);
mAccessibilityManager.sendAccessibilityEvent(event);
}
}

View File

@ -152,6 +152,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private boolean mIsSettingsSuggestionStripOn;
private boolean mApplicationSpecifiedCompletionOn;
private AccessibilityUtils mAccessibilityUtils;
private final StringBuilder mComposing = new StringBuilder();
private WordComposer mWord = new WordComposer();
private CharSequence mBestWord;
@ -357,6 +359,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
LatinImeLogger.init(this, prefs);
SubtypeSwitcher.init(this, prefs);
KeyboardSwitcher.init(this, prefs);
AccessibilityUtils.init(this, prefs);
super.onCreate();
@ -364,6 +367,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mInputMethodId = Utils.getInputMethodId(mImm, getPackageName());
mSubtypeSwitcher = SubtypeSwitcher.getInstance();
mKeyboardSwitcher = KeyboardSwitcher.getInstance();
mAccessibilityUtils = AccessibilityUtils.getInstance();
final Resources res = getResources();
mResources = res;
@ -546,8 +550,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
updateCorrectionMode();
final boolean accessibilityEnabled = mAccessibilityUtils.isAccessibilityEnabled();
inputView.setPreviewEnabled(mPopupOn);
inputView.setProximityCorrectionEnabled(true);
inputView.setAccessibilityEnabled(accessibilityEnabled);
// If we just entered a text field, maybe it has some old text that requires correction
checkReCorrectionOnStart();
inputView.setForeground(true);
@ -1073,6 +1080,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
mLastKeyTime = when;
KeyboardSwitcher switcher = mKeyboardSwitcher;
final boolean accessibilityEnabled = switcher.isAccessibilityEnabled();
final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
switch (primaryCode) {
case Keyboard.CODE_DELETE:
@ -1082,12 +1090,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
break;
case Keyboard.CODE_SHIFT:
// Shift key is handled in onPress() when device has distinct multi-touch panel.
if (!distinctMultiTouch)
if (!distinctMultiTouch || accessibilityEnabled)
switcher.toggleShift();
break;
case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
// Symbol key is handled in onPress() when device has distinct multi-touch panel.
if (!distinctMultiTouch)
if (!distinctMultiTouch || accessibilityEnabled)
switcher.changeKeyboardMode();
break;
case Keyboard.CODE_CANCEL:
@ -1913,6 +1921,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
} else {
switcher.onOtherKeyPressed();
}
mAccessibilityUtils.onPress(primaryCode, switcher);
}
@Override
@ -1926,6 +1935,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
} else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
switcher.onReleaseSymbol();
}
mAccessibilityUtils.onRelease(primaryCode, switcher);
}