diff options
Diffstat (limited to 'java/src/com/android/inputmethod/accessibility')
6 files changed, 773 insertions, 389 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java new file mode 100644 index 000000000..70e38fdb0 --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java @@ -0,0 +1,342 @@ +/* + * 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.inputmethodservice.InputMethodService; +import android.os.Bundle; +import android.os.SystemClock; +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.util.SparseArray; +import android.view.MotionEvent; +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; + +/** + * 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> + */ +public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat { + private static final String TAG = AccessibilityEntityProvider.class.getSimpleName(); + private static final int UNDEFINED = Integer.MIN_VALUE; + + private final InputMethodService mInputMethodService; + private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper; + private final AccessibilityUtils mAccessibilityUtils; + + /** A map of integer IDs to {@link Key}s. */ + private final SparseArray<Key> mVirtualViewIdToKey = new SparseArray<Key>(); + + /** 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 = new int[2]; + + /** The virtual view identifier for the focused node. */ + private int mAccessibilityFocusedView = UNDEFINED; + + /** The current keyboard view. */ + private KeyboardView mKeyboardView; + + public AccessibilityEntityProvider(KeyboardView keyboardView, InputMethodService inputMethod) { + mInputMethodService = inputMethod; + + mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance(); + mAccessibilityUtils = AccessibilityUtils.getInstance(); + + setView(keyboardView); + } + + /** + * Sets the keyboard view represented by this node provider. + * + * @param keyboardView The keyboard view to represent. + */ + public void setView(KeyboardView keyboardView) { + mKeyboardView = keyboardView; + updateParentLocation(); + + // Since this class is constructed lazily, we might not get a subsequent + // call to setKeyboard() and therefore need to call it now. + setKeyboard(mKeyboardView.getKeyboard()); + } + + /** + * Sets the keyboard represented by this node provider. + * + * @param keyboard The keyboard to represent. + */ + public void setKeyboard(Keyboard keyboard) { + assignVirtualViewIds(); + } + + /** + * 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(Key key, int eventType) { + final int virtualViewId = generateVirtualViewIdForKey(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 = new AccessibilityRecordCompat(event); + record.setSource(mKeyboardView, virtualViewId); + + return event; + } + + /** + * 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(int virtualViewId) { + AccessibilityNodeInfoCompat info = null; + + if (virtualViewId == UNDEFINED) { + return null; + } else if (virtualViewId == View.NO_ID) { + // We are requested to create an AccessibilityNodeInfo describing + // this View, i.e. the root of the virtual sub-tree. + info = AccessibilityNodeInfoCompat.obtain(mKeyboardView); + ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, info); + + // Add the virtual children of the root View. + final Keyboard keyboard = mKeyboardView.getKeyboard(); + final Key[] keys = keyboard.mKeys; + for (Key key : keys) { + final int childVirtualViewId = generateVirtualViewIdForKey(key); + info.addChild(mKeyboardView, childVirtualViewId); + } + } else { + // Find the view that corresponds to the given id. + final Key key = mVirtualViewIdToKey.get(virtualViewId); + if (key == null) { + Log.e(TAG, "Invalid virtual view ID: " + virtualViewId); + return null; + } + + final String keyDescription = getKeyDescription(key); + final Rect boundsInParent = key.mHitBox; + + // Calculate the key's in-screen bounds. + mTempBoundsInScreen.set(boundsInParent); + mTempBoundsInScreen.offset(mParentLocation[0], mParentLocation[1]); + + final Rect boundsInScreen = mTempBoundsInScreen; + + // Obtain and initialize an AccessibilityNodeInfo with + // information about the virtual view. + 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.setBoundsInScreen(boundsInScreen); + 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; + } + + /** + * 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.mHitBox.centerX(); + final int y = key.mHitBox.centerY(); + 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; + } + + /** + * 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); + 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(Key key) { + final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo(); + final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo); + final String keyDescription = mKeyCodeDescriptionMapper.getDescriptionForKey( + mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure); + + return keyDescription; + } + + /** + * Assigns virtual view IDs to keyboard keys and populates the related maps. + */ + private void assignVirtualViewIds() { + final Keyboard keyboard = mKeyboardView.getKeyboard(); + if (keyboard == null) { + return; + } + + mVirtualViewIdToKey.clear(); + + final Key[] keys = keyboard.mKeys; + for (Key key : keys) { + final int virtualViewId = generateVirtualViewIdForKey(key); + mVirtualViewIdToKey.put(virtualViewId, key); + } + } + + /** + * Updates the parent's on-screen location. + */ + private void updateParentLocation() { + mKeyboardView.getLocationOnScreen(mParentLocation); + } + + /** + * Generates a virtual view identifier for the given key. Returned + * identifiers are valid until the next global layout state change. + * + * @param key The key to identify. + * @return A virtual view identifier. + */ + private static int generateVirtualViewIdForKey(Key key) { + // The key x- and y-coordinates are stable between layout changes. + // Generate an identifier by bit-shifting the x-coordinate to the + // left-half of the integer and OR'ing with the y-coordinate. + return ((0xFFFF & key.mX) << (Integer.SIZE / 2)) | (0xFFFF & key.mY); + } +} diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java index ae614b7e0..616b1c6d7 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java @@ -16,19 +16,21 @@ package com.android.inputmethod.accessibility; -import android.accessibilityservice.AccessibilityServiceInfo; import android.content.Context; -import android.content.SharedPreferences; import android.inputmethodservice.InputMethodService; +import android.media.AudioManager; import android.os.SystemClock; +import android.provider.Settings; import android.util.Log; import android.view.MotionEvent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.EditorInfo; -import com.android.inputmethod.compat.AccessibilityEventCompatUtils; -import com.android.inputmethod.compat.AccessibilityManagerCompatWrapper; -import com.android.inputmethod.compat.MotionEventCompatUtils; +import com.android.inputmethod.compat.AudioManagerCompatWrapper; +import com.android.inputmethod.compat.SettingsSecureCompatUtils; +import com.android.inputmethod.latin.InputTypeUtils; +import com.android.inputmethod.latin.R; public class AccessibilityUtils { private static final String TAG = AccessibilityUtils.class.getSimpleName(); @@ -38,8 +40,9 @@ public class AccessibilityUtils { private static final AccessibilityUtils sInstance = new AccessibilityUtils(); + private Context mContext; private AccessibilityManager mAccessibilityManager; - private AccessibilityManagerCompatWrapper mCompatManager; + private AudioManagerCompatWrapper mAudioManager; /* * Setting this constant to {@code false} will disable all keyboard @@ -48,15 +51,14 @@ public class AccessibilityUtils { */ private static final boolean ENABLE_ACCESSIBILITY = true; - public static void init(InputMethodService inputMethod, SharedPreferences prefs) { + public static void init(InputMethodService inputMethod) { if (!ENABLE_ACCESSIBILITY) return; // These only need to be initialized if the kill switch is off. - sInstance.initInternal(inputMethod, prefs); - KeyCodeDescriptionMapper.init(inputMethod, prefs); - AccessibleInputMethodServiceProxy.init(inputMethod, prefs); - AccessibleKeyboardViewProxy.init(inputMethod, prefs); + sInstance.initInternal(inputMethod); + KeyCodeDescriptionMapper.init(); + AccessibleKeyboardViewProxy.init(inputMethod); } public static AccessibilityUtils getInstance() { @@ -67,10 +69,14 @@ public class AccessibilityUtils { // This class is not publicly instantiable. } - private void initInternal(Context context, SharedPreferences prefs) { + private void initInternal(Context context) { + mContext = context; mAccessibilityManager = (AccessibilityManager) context .getSystemService(Context.ACCESSIBILITY_SERVICE); - mCompatManager = new AccessibilityManagerCompatWrapper(mAccessibilityManager); + + final AudioManager audioManager = (AudioManager) context + .getSystemService(Context.AUDIO_SERVICE); + mAudioManager = new AudioManagerCompatWrapper(audioManager); } /** @@ -82,10 +88,8 @@ public class AccessibilityUtils { */ public boolean isTouchExplorationEnabled() { return ENABLE_ACCESSIBILITY - && AccessibilityEventCompatUtils.supportsTouchExploration() && mAccessibilityManager.isEnabled() - && !mCompatManager.getEnabledAccessibilityServiceList( - AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty(); + && mAccessibilityManager.isTouchExplorationEnabled(); } /** @@ -99,9 +103,35 @@ public class AccessibilityUtils { public boolean isTouchExplorationEvent(MotionEvent event) { final int action = event.getAction(); - return action == MotionEventCompatUtils.ACTION_HOVER_ENTER - || action == MotionEventCompatUtils.ACTION_HOVER_EXIT - || action == MotionEventCompatUtils.ACTION_HOVER_MOVE; + return action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_EXIT + || action == MotionEvent.ACTION_HOVER_MOVE; + } + + /** + * Returns whether the device should obscure typed password characters. + * Typically this means speaking "dot" in place of non-control characters. + * + * @return {@code true} if the device should obscure password characters. + */ + public boolean shouldObscureInput(EditorInfo editorInfo) { + if (editorInfo == null) + return false; + + // The user can optionally force speaking passwords. + if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) { + final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(), + SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0; + if (speakPassword) + return false; + } + + // Always speak if the user is listening through headphones. + if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) + return false; + + // Don't speak if the IME is connected to a password field. + return InputTypeUtils.isPasswordInputType(editorInfo.inputType); } /** @@ -120,7 +150,7 @@ public class AccessibilityUtils { // class. Instead, we're just forcing a fake AccessibilityEvent into // the screen reader to make it speak. final AccessibilityEvent event = AccessibilityEvent - .obtain(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER); + .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); event.setPackageName(PACKAGE); event.setClassName(CLASS); @@ -130,4 +160,30 @@ public class AccessibilityUtils { mAccessibilityManager.sendAccessibilityEvent(event); } + + /** + * Handles speaking the "connect a headset to hear passwords" notification + * when connecting to a password field. + * + * @param editorInfo The input connection's editor info attribute. + * @param restarting Whether the connection is being restarted. + */ + public void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) { + if (shouldObscureInput(editorInfo)) { + final CharSequence text = mContext.getText(R.string.spoken_use_headphones); + speak(text); + } + } + + /** + * Sends the specified {@link AccessibilityEvent} if accessibility is + * enabled. No operation if accessibility is disabled. + * + * @param event The event to send. + */ + public void requestSendAccessibilityEvent(AccessibilityEvent event) { + if (mAccessibilityManager.isEnabled()) { + mAccessibilityManager.sendAccessibilityEvent(event); + } + } } diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java deleted file mode 100644 index 7199550a9..000000000 --- a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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.accessibility; - -import android.content.SharedPreferences; -import android.inputmethodservice.InputMethodService; -import android.os.Looper; -import android.os.Message; -import android.text.TextUtils; -import android.view.inputmethod.ExtractedText; -import android.view.inputmethod.ExtractedTextRequest; - -import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.StaticInnerHandlerWrapper; - -public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActionListener { - private static final AccessibleInputMethodServiceProxy sInstance = - new AccessibleInputMethodServiceProxy(); - - /* - * Delay for the handler event that's fired when Accessibility is on and the - * user hovers outside of any valid keys. This is used to let the user know - * that if they lift their finger, nothing will be typed. - */ - private static final long DELAY_NO_HOVER_SELECTION = 250; - - private InputMethodService mInputMethod; - - private AccessibilityHandler mAccessibilityHandler; - - private static class AccessibilityHandler - extends StaticInnerHandlerWrapper<AccessibleInputMethodServiceProxy> { - private static final int MSG_NO_HOVER_SELECTION = 0; - - public AccessibilityHandler(AccessibleInputMethodServiceProxy outerInstance, - Looper looper) { - super(outerInstance, looper); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_NO_HOVER_SELECTION: - getOuterInstance().notifyNoHoverSelection(); - break; - } - } - - public void postNoHoverSelection() { - removeMessages(MSG_NO_HOVER_SELECTION); - sendEmptyMessageDelayed(MSG_NO_HOVER_SELECTION, DELAY_NO_HOVER_SELECTION); - } - - public void cancelNoHoverSelection() { - removeMessages(MSG_NO_HOVER_SELECTION); - } - } - - public static void init(InputMethodService inputMethod, SharedPreferences prefs) { - sInstance.initInternal(inputMethod, prefs); - } - - public static AccessibleInputMethodServiceProxy getInstance() { - return sInstance; - } - - private AccessibleInputMethodServiceProxy() { - // Not publicly instantiable. - } - - private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) { - mInputMethod = inputMethod; - mAccessibilityHandler = new AccessibilityHandler(this, inputMethod.getMainLooper()); - } - - /** - * If touch exploration is enabled, cancels the event sent by - * {@link AccessibleInputMethodServiceProxy#onHoverExit(int)} because the - * user is currently hovering above a key. - */ - @Override - public void onHoverEnter(int primaryCode) { - mAccessibilityHandler.cancelNoHoverSelection(); - } - - /** - * If touch exploration is enabled, sends a delayed event to notify the user - * that they are not currently hovering above a key. - */ - @Override - public void onHoverExit(int primaryCode) { - mAccessibilityHandler.postNoHoverSelection(); - } - - /** - * When Accessibility is turned on, notifies the user that they are not - * currently hovering above a key. By default this will speak the currently - * entered text. - */ - private void notifyNoHoverSelection() { - final ExtractedText extracted = mInputMethod.getCurrentInputConnection().getExtractedText( - new ExtractedTextRequest(), 0); - - if (extracted == null) - return; - - final CharSequence text; - - if (TextUtils.isEmpty(extracted.text)) { - text = mInputMethod.getString(R.string.spoken_no_text_entered); - } else { - text = mInputMethod.getString(R.string.spoken_current_text_is, extracted.text); - } - - AccessibilityUtils.getInstance().speak(text); - } -} diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java deleted file mode 100644 index 12c59d0fc..000000000 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.accessibility; - -public interface AccessibleKeyboardActionListener { - /** - * Called when the user hovers inside a key. This is sent only when - * Accessibility is turned on. For keys that repeat, this is only called - * once. - * - * @param primaryCode the code of the key that was hovered over - */ - public void onHoverEnter(int primaryCode); - - /** - * Called when the user hovers outside a key. This is sent only when - * Accessibility is turned on. For keys that repeat, this is only called - * once. - * - * @param primaryCode the code of the key that was hovered over - */ - public void onHoverExit(int primaryCode); -} diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java index 96f7fc9f2..f6376d5f4 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java @@ -17,185 +17,279 @@ package com.android.inputmethod.accessibility; import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Color; -import android.graphics.Paint; -import android.util.Log; +import android.inputmethodservice.InputMethodService; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.view.MotionEvent; +import android.view.View; import android.view.ViewConfiguration; -import android.view.accessibility.AccessibilityEvent; -import com.android.inputmethod.compat.AccessibilityEventCompatUtils; -import com.android.inputmethod.compat.MotionEventCompatUtils; import com.android.inputmethod.keyboard.Key; -import com.android.inputmethod.keyboard.KeyDetector; -import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardId; +import com.android.inputmethod.keyboard.LatinKeyboardView; import com.android.inputmethod.keyboard.PointerTracker; +import com.android.inputmethod.latin.R; -public class AccessibleKeyboardViewProxy { - private static final String TAG = AccessibleKeyboardViewProxy.class.getSimpleName(); +public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy(); - // Delay in milliseconds between key press DOWN and UP events - private static final long DELAY_KEY_PRESS = 10; + private InputMethodService mInputMethod; + private LatinKeyboardView mView; + private AccessibilityEntityProvider mAccessibilityNodeProvider; - private int mScaledEdgeSlop; - private KeyboardView mView; - private AccessibleKeyboardActionListener mListener; + private Key mLastHoverKey = null; - private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY; - private int mLastX = -1; - private int mLastY = -1; + /** + * Inset in pixels to look for keys when the user's finger exits the + * keyboard area. See {@link ViewConfiguration#getScaledEdgeSlop()}. + */ + private int mEdgeSlop; - public static void init(Context context, SharedPreferences prefs) { - sInstance.initInternal(context, prefs); - sInstance.mListener = AccessibleInputMethodServiceProxy.getInstance(); + public static void init(InputMethodService inputMethod) { + sInstance.initInternal(inputMethod); } public static AccessibleKeyboardViewProxy getInstance() { return sInstance; } - public static void setView(KeyboardView view) { - sInstance.mView = view; - } - private AccessibleKeyboardViewProxy() { // Not publicly instantiable. } - private void initInternal(Context context, SharedPreferences prefs) { - final Paint paint = new Paint(); - paint.setTextAlign(Paint.Align.LEFT); - paint.setTextSize(14.0f); - paint.setAntiAlias(true); - paint.setColor(Color.YELLOW); - - mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop(); + private void initInternal(InputMethodService inputMethod) { + mInputMethod = inputMethod; + mEdgeSlop = ViewConfiguration.get(inputMethod).getScaledEdgeSlop(); } - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event, - PointerTracker tracker) { - if (mView == null) { - Log.e(TAG, "No keyboard view set!"); - return false; + /** + * Sets the view wrapped by this proxy. + * + * @param view The view to wrap. + */ + public void setView(LatinKeyboardView view) { + if (view == null) { + // Ignore null views. + return; } - switch (event.getEventType()) { - case AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER: - final Key key = tracker.getKey(mLastHoverKeyIndex); + mView = view; - if (key == null) - break; + // Ensure that the view has an accessibility delegate. + ViewCompat.setAccessibilityDelegate(view, this); - final CharSequence description = KeyCodeDescriptionMapper.getInstance() - .getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key); - - if (description == null) - return false; - - event.getText().add(description); - - break; + if (mAccessibilityNodeProvider != null) { + mAccessibilityNodeProvider.setView(view); } + } - return true; + public void setKeyboard(Keyboard keyboard) { + if (mAccessibilityNodeProvider != null) { + mAccessibilityNodeProvider.setKeyboard(keyboard); + } } /** - * Receives hover events when accessibility is turned on in API > 11. In - * earlier API levels, events are manually routed from onTouchEvent. + * Proxy method for View.getAccessibilityNodeProvider(). This method is + * called in SDK version 15 and higher to obtain the virtual node hierarchy + * provider. * - * @param event The hover event. - * @return {@code true} if the event is handled + * @return The accessibility node provider for the current keyboard. */ - public boolean onHoverEvent(MotionEvent event, PointerTracker tracker) { - return onTouchExplorationEvent(event, tracker); - } - - public boolean dispatchTouchEvent(MotionEvent event) { - // Since touch exploration translates hover double-tap to a regular - // single-tap, we're going to drop non-touch exploration events. - if (!AccessibilityUtils.getInstance().isTouchExplorationEvent(event)) - return true; - - return false; + @Override + public AccessibilityEntityProvider getAccessibilityNodeProvider(View host) { + return getAccessibilityNodeProvider(); } /** - * Handles touch exploration events when Accessibility is turned on. + * Receives hover events when accessibility is turned on in SDK versions ICS + * and higher. * - * @param event The touch exploration hover event. - * @return {@code true} if the event was handled + * @param event The hover event. + * @return {@code true} if the event is handled */ - private boolean onTouchExplorationEvent(MotionEvent event, PointerTracker tracker) { + public boolean dispatchHoverEvent(MotionEvent event, PointerTracker tracker) { final int x = (int) event.getX(); final int y = (int) event.getY(); + final Key key = tracker.getKeyOn(x, y); + final Key previousKey = mLastHoverKey; + + mLastHoverKey = key; switch (event.getAction()) { - case MotionEventCompatUtils.ACTION_HOVER_ENTER: - case MotionEventCompatUtils.ACTION_HOVER_MOVE: - final int keyIndex = tracker.getKeyIndexOn(x, y); - - if (keyIndex != mLastHoverKeyIndex) { - fireKeyHoverEvent(tracker, mLastHoverKeyIndex, false); - mLastHoverKeyIndex = keyIndex; - mLastX = x; - mLastY = y; - fireKeyHoverEvent(tracker, mLastHoverKeyIndex, true); + case MotionEvent.ACTION_HOVER_EXIT: + // Make sure we're not getting an EXIT event because the user slid + // off the keyboard area, then force a key press. + if (pointInView(x, y) && (key != null)) { + getAccessibilityNodeProvider().simulateKeyPress(key); } - - return true; - case MotionEventCompatUtils.ACTION_HOVER_EXIT: - final int width = mView.getWidth(); - final int height = mView.getHeight(); - - if (x < mScaledEdgeSlop || y < mScaledEdgeSlop || x >= (width - mScaledEdgeSlop) - || y >= (height - mScaledEdgeSlop)) { - fireKeyHoverEvent(tracker, mLastHoverKeyIndex, false); - mLastHoverKeyIndex = KeyDetector.NOT_A_KEY; - mLastX = -1; - mLastY = -1; - } else if (mLastHoverKeyIndex != KeyDetector.NOT_A_KEY) { - fireKeyPressEvent(tracker, mLastX, mLastY, event.getEventTime()); + //$FALL-THROUGH$ + case MotionEvent.ACTION_HOVER_ENTER: + return onHoverKey(key, event); + case MotionEvent.ACTION_HOVER_MOVE: + if (key != previousKey) { + return onTransitionKey(key, previousKey, event); + } else { + return onHoverKey(key, event); } - - return true; } return false; } - private void fireKeyHoverEvent(PointerTracker tracker, int keyIndex, boolean entering) { - if (mListener == null) { - Log.e(TAG, "No accessible keyboard action listener set!"); - return; + /** + * @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; + } - if (mView == null) { - Log.e(TAG, "No keyboard view set!"); - return; + /** + * Utility method to determine whether the given point, in local + * coordinates, is inside the view, where the area of the view is contracted + * by the edge slop factor. + * + * @param localX The local x-coordinate. + * @param localY The local y-coordinate. + */ + private boolean pointInView(int localX, int localY) { + return (localX >= mEdgeSlop) && (localY >= mEdgeSlop) + && (localX < (mView.getWidth() - mEdgeSlop)) + && (localY < (mView.getHeight() - mEdgeSlop)); + } + + /** + * Simulates a transition between two {@link Key}s by sending a HOVER_EXIT + * on the previous key, a HOVER_ENTER on the current key, and a HOVER_MOVE + * on the current key. + * + * @param currentKey The currently hovered key. + * @param previousKey The previously hovered key. + * @param event The event that triggered the transition. + * @return {@code true} if the event was handled. + */ + private boolean onTransitionKey(Key currentKey, Key previousKey, MotionEvent event) { + final int savedAction = event.getAction(); + + event.setAction(MotionEvent.ACTION_HOVER_EXIT); + onHoverKey(previousKey, event); + + event.setAction(MotionEvent.ACTION_HOVER_ENTER); + onHoverKey(currentKey, event); + + event.setAction(MotionEvent.ACTION_HOVER_MOVE); + final boolean handled = onHoverKey(currentKey, event); + + event.setAction(savedAction); + + return handled; + } + + /** + * Handles a hover event on a key. If {@link Key} extended View, this would + * be analogous to calling View.onHoverEvent(MotionEvent). + * + * @param key The currently hovered key. + * @param event The hover event. + * @return {@code true} if the event was handled. + */ + private boolean onHoverKey(Key key, MotionEvent event) { + // Null keys can't receive events. + if (key == null) { + return false; } - if (keyIndex == KeyDetector.NOT_A_KEY) - return; + final AccessibilityEntityProvider provider = getAccessibilityNodeProvider(); - final Key key = tracker.getKey(keyIndex); + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + provider.sendAccessibilityEventForKey( + key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER); + provider.performActionForKey( + key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); + break; + case MotionEvent.ACTION_HOVER_EXIT: + provider.sendAccessibilityEventForKey( + key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT); + break; + } - if (key == null) - return; + return true; + } - if (entering) { - mListener.onHoverEnter(key.mCode); - mView.sendAccessibilityEvent(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER); - } else { - mListener.onHoverExit(key.mCode); - mView.sendAccessibilityEvent(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_EXIT); + /** + * Notifies the user of changes in the keyboard shift state. + */ + public void notifyShiftState() { + final Keyboard keyboard = mView.getKeyboard(); + final KeyboardId keyboardId = keyboard.mId; + final int elementId = keyboardId.mElementId; + final Context context = mView.getContext(); + final CharSequence text; + + switch (elementId) { + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: + text = context.getText(R.string.spoken_description_shiftmode_locked); + break; + case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: + case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: + text = context.getText(R.string.spoken_description_shiftmode_on); + break; + default: + text = context.getText(R.string.spoken_description_shiftmode_off); } + + AccessibilityUtils.getInstance().speak(text); } - private void fireKeyPressEvent(PointerTracker tracker, int x, int y, long eventTime) { - tracker.onDownEvent(x, y, eventTime, null); - tracker.onUpEvent(x, y, eventTime + DELAY_KEY_PRESS, null); + /** + * Notifies the user of changes in the keyboard symbols state. + */ + public void notifySymbolsState() { + final Keyboard keyboard = mView.getKeyboard(); + final Context context = mView.getContext(); + final KeyboardId keyboardId = keyboard.mId; + final int elementId = keyboardId.mElementId; + final int resId; + + switch (elementId) { + case KeyboardId.ELEMENT_ALPHABET: + case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: + resId = R.string.spoken_description_mode_alpha; + break; + case KeyboardId.ELEMENT_SYMBOLS: + case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: + resId = R.string.spoken_description_mode_symbol; + break; + case KeyboardId.ELEMENT_PHONE: + resId = R.string.spoken_description_mode_phone; + break; + case KeyboardId.ELEMENT_PHONE_SYMBOLS: + resId = R.string.spoken_description_mode_phone_shift; + break; + default: + resId = -1; + } + + if (resId < 0) { + return; + } + + final String text = context.getString(resId); + AccessibilityUtils.getInstance().speak(text); } } diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java index a31911d60..23acb8b74 100644 --- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java +++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java @@ -17,8 +17,9 @@ package com.android.inputmethod.accessibility; import android.content.Context; -import android.content.SharedPreferences; import android.text.TextUtils; +import android.util.Log; +import android.view.inputmethod.EditorInfo; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; @@ -28,6 +29,11 @@ import com.android.inputmethod.latin.R; import java.util.HashMap; public class KeyCodeDescriptionMapper { + private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName(); + + // The resource ID of the string spoken for obscured keys + private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot; + private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper(); // Map of key labels to spoken description resource IDs @@ -36,14 +42,8 @@ public class KeyCodeDescriptionMapper { // Map of key codes to spoken description resource IDs private final HashMap<Integer, Integer> mKeyCodeMap; - // Map of shifted key codes to spoken description resource IDs - private final HashMap<Integer, Integer> mShiftedKeyCodeMap; - - // Map of shift-locked key codes to spoken description resource IDs - private final HashMap<Integer, Integer> mShiftLockedKeyCodeMap; - - public static void init(Context context, SharedPreferences prefs) { - sInstance.initInternal(context, prefs); + public static void init() { + sInstance.initInternal(); } public static KeyCodeDescriptionMapper getInstance() { @@ -53,39 +53,15 @@ public class KeyCodeDescriptionMapper { private KeyCodeDescriptionMapper() { mKeyLabelMap = new HashMap<CharSequence, Integer>(); mKeyCodeMap = new HashMap<Integer, Integer>(); - mShiftedKeyCodeMap = new HashMap<Integer, Integer>(); - mShiftLockedKeyCodeMap = new HashMap<Integer, Integer>(); } - private void initInternal(Context context, SharedPreferences prefs) { + private void initInternal() { // Manual label substitutions for key labels with no string resource mKeyLabelMap.put(":-)", R.string.spoken_description_smiley); // Symbols that most TTS engines can't speak - mKeyCodeMap.put((int) '.', R.string.spoken_description_period); - mKeyCodeMap.put((int) ',', R.string.spoken_description_comma); - mKeyCodeMap.put((int) '(', R.string.spoken_description_left_parenthesis); - mKeyCodeMap.put((int) ')', R.string.spoken_description_right_parenthesis); - mKeyCodeMap.put((int) ':', R.string.spoken_description_colon); - mKeyCodeMap.put((int) ';', R.string.spoken_description_semicolon); - mKeyCodeMap.put((int) '!', R.string.spoken_description_exclamation_mark); - mKeyCodeMap.put((int) '?', R.string.spoken_description_question_mark); - mKeyCodeMap.put((int) '\"', R.string.spoken_description_double_quote); - mKeyCodeMap.put((int) '\'', R.string.spoken_description_single_quote); - mKeyCodeMap.put((int) '*', R.string.spoken_description_star); - mKeyCodeMap.put((int) '#', R.string.spoken_description_pound); mKeyCodeMap.put((int) ' ', R.string.spoken_description_space); - // Non-ASCII symbols (must use escape codes!) - mKeyCodeMap.put((int) '\u2022', R.string.spoken_description_dot); - mKeyCodeMap.put((int) '\u221A', R.string.spoken_description_square_root); - mKeyCodeMap.put((int) '\u03C0', R.string.spoken_description_pi); - mKeyCodeMap.put((int) '\u0394', R.string.spoken_description_delta); - mKeyCodeMap.put((int) '\u2122', R.string.spoken_description_trademark); - mKeyCodeMap.put((int) '\u2105', R.string.spoken_description_care_of); - mKeyCodeMap.put((int) '\u2026', R.string.spoken_description_ellipsis); - mKeyCodeMap.put((int) '\u201E', R.string.spoken_description_low_double_quote); - // Special non-character codes defined in Keyboard mKeyCodeMap.put(Keyboard.CODE_DELETE, R.string.spoken_description_delete); mKeyCodeMap.put(Keyboard.CODE_ENTER, R.string.spoken_description_return); @@ -94,12 +70,6 @@ public class KeyCodeDescriptionMapper { mKeyCodeMap.put(Keyboard.CODE_SHORTCUT, R.string.spoken_description_mic); mKeyCodeMap.put(Keyboard.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol); mKeyCodeMap.put(Keyboard.CODE_TAB, R.string.spoken_description_tab); - - // Shifted versions of non-character codes defined in Keyboard - mShiftedKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_shift_shifted); - - // Shift-locked versions of non-character codes defined in Keyboard - mShiftLockedKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_caps_lock); } /** @@ -117,30 +87,40 @@ public class KeyCodeDescriptionMapper { * @param context The package's context. * @param keyboard The keyboard on which the key resides. * @param key The key from which to obtain a description. + * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. * @return a character sequence describing the action performed by pressing * the key */ - public CharSequence getDescriptionForKey(Context context, Keyboard keyboard, Key key) { - if (key.mCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { - final CharSequence description = getDescriptionForSwitchAlphaSymbol(context, keyboard); + public String getDescriptionForKey(Context context, Keyboard keyboard, Key key, + boolean shouldObscure) { + final int code = key.mCode; + + if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { + final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard); if (description != null) return description; } + if (code == Keyboard.CODE_SHIFT) { + return getDescriptionForShiftKey(context, keyboard); + } + + if (code == Keyboard.CODE_ACTION_ENTER) { + return getDescriptionForActionKey(context, keyboard, key); + } + if (!TextUtils.isEmpty(key.mLabel)) { final String label = key.mLabel.toString().trim(); + // First, attempt to map the label to a pre-defined description. if (mKeyLabelMap.containsKey(label)) { return context.getString(mKeyLabelMap.get(label)); - } else if (label.length() == 1 - || (keyboard.isManualTemporaryUpperCase() && !TextUtils - .isEmpty(key.mHintLabel))) { - return getDescriptionForKeyCode(context, keyboard, key); - } else { - return label; } - } else if (key.mCode != Keyboard.CODE_DUMMY) { - return getDescriptionForKeyCode(context, keyboard, key); + } + + // Just attempt to speak the description. + if (key.mCode != Keyboard.CODE_UNSPECIFIED) { + return getDescriptionForKeyCode(context, keyboard, key, shouldObscure); } return null; @@ -156,36 +136,110 @@ public class KeyCodeDescriptionMapper { * @return a character sequence describing the action performed by pressing * the key */ - private CharSequence getDescriptionForSwitchAlphaSymbol(Context context, Keyboard keyboard) { - final KeyboardId id = keyboard.mId; - - if (id.isAlphabetKeyboard()) { - return context.getString(R.string.spoken_description_to_symbol); - } else if (id.isSymbolsKeyboard()) { - return context.getString(R.string.spoken_description_to_alpha); - } else if (id.isPhoneSymbolsKeyboard()) { - return context.getString(R.string.spoken_description_to_numeric); - } else if (id.isPhoneKeyboard()) { - return context.getString(R.string.spoken_description_to_symbol); - } else { + private String getDescriptionForSwitchAlphaSymbol(Context context, Keyboard keyboard) { + final KeyboardId keyboardId = keyboard.mId; + final int elementId = keyboardId.mElementId; + final int resId; + + switch (elementId) { + case KeyboardId.ELEMENT_ALPHABET: + case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: + resId = R.string.spoken_description_to_symbol; + break; + case KeyboardId.ELEMENT_SYMBOLS: + case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: + resId = R.string.spoken_description_to_alpha; + break; + case KeyboardId.ELEMENT_PHONE: + resId = R.string.spoken_description_to_symbol; + break; + case KeyboardId.ELEMENT_PHONE_SYMBOLS: + resId = R.string.spoken_description_to_numeric; + break; + default: + Log.e(TAG, "Missing description for keyboard element ID:" + elementId); return null; } + + return context.getString(resId); } /** - * Returns the keycode for the specified key given the current keyboard - * state. + * Returns a context-sensitive description of the "Shift" key. * + * @param context The package's context. * @param keyboard The keyboard on which the key resides. - * @param key The key from which to obtain a key code. - * @return the key code for the specified key + * @return A context-sensitive description of the "Shift" key. */ - private int getCorrectKeyCode(Keyboard keyboard, Key key) { - if (keyboard.isManualTemporaryUpperCase() && !TextUtils.isEmpty(key.mHintLabel)) { - return key.mHintLabel.charAt(0); - } else { - return key.mCode; + private String getDescriptionForShiftKey(Context context, Keyboard keyboard) { + final KeyboardId keyboardId = keyboard.mId; + final int elementId = keyboardId.mElementId; + final int resId; + + switch (elementId) { + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: + resId = R.string.spoken_description_caps_lock; + break; + case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: + case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: + resId = R.string.spoken_description_shift_shifted; + break; + default: + resId = R.string.spoken_description_shift; + } + + return context.getString(resId); + } + + /** + * Returns a context-sensitive description of the "Enter" action key. + * + * @param context The package's context. + * @param keyboard The keyboard on which the key resides. + * @param key The key to describe. + * @return Returns a context-sensitive description of the "Enter" action + * key. + */ + private String getDescriptionForActionKey(Context context, Keyboard keyboard, Key key) { + final KeyboardId keyboardId = keyboard.mId; + final int actionId = keyboardId.imeActionId(); + final int resId; + + // Always use the label, if available. + if (!TextUtils.isEmpty(key.mLabel)) { + return key.mLabel.toString().trim(); + } + + // Otherwise, use the action ID. + switch (actionId) { + case EditorInfo.IME_ACTION_SEARCH: + resId = R.string.spoken_description_search; + break; + case EditorInfo.IME_ACTION_GO: + resId = R.string.label_go_key; + break; + case EditorInfo.IME_ACTION_SEND: + resId = R.string.label_send_key; + break; + case EditorInfo.IME_ACTION_NEXT: + resId = R.string.label_next_key; + break; + case EditorInfo.IME_ACTION_DONE: + resId = R.string.label_done_key; + break; + case EditorInfo.IME_ACTION_PREVIOUS: + resId = R.string.label_previous_key; + break; + default: + resId = R.string.spoken_description_return; } + + return context.getString(resId); } /** @@ -205,20 +259,26 @@ public class KeyCodeDescriptionMapper { * @param context The package's context. * @param keyboard The keyboard on which the key resides. * @param key The key from which to obtain a description. + * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. * @return a character sequence describing the action performed by pressing * the key */ - private CharSequence getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key) { - final int code = getCorrectKeyCode(keyboard, key); - - if (keyboard.isShiftLocked() && mShiftLockedKeyCodeMap.containsKey(code)) { - return context.getString(mShiftLockedKeyCodeMap.get(code)); - } else if (keyboard.isShiftedOrShiftLocked() && mShiftedKeyCodeMap.containsKey(code)) { - return context.getString(mShiftedKeyCodeMap.get(code)); - } else if (mKeyCodeMap.containsKey(code)) { + private String getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key, + boolean shouldObscure) { + final int code = key.mCode; + + // If the key description should be obscured, now is the time to do it. + final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code); + if (shouldObscure && isDefinedNonCtrl) { + return context.getString(OBSCURED_KEY_RES_ID); + } + + if (mKeyCodeMap.containsKey(code)) { return context.getString(mKeyCodeMap.get(code)); - } else if (Character.isDefined(code) && !Character.isISOControl(code)) { + } else if (isDefinedNonCtrl) { return Character.toString((char) code); + } else if (!TextUtils.isEmpty(key.mLabel)) { + return key.mLabel; } else { return context.getString(R.string.spoken_description_unknown, code); } |