diff options
Diffstat (limited to 'java/src/com/android/inputmethod/accessibility')
-rw-r--r-- | java/src/com/android/inputmethod/accessibility/AccessibilityLongPressTimer.java | 67 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java | 11 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java | 411 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java | 197 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java | 327 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java (renamed from java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java) | 221 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java | 303 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java | 120 |
8 files changed, 1060 insertions, 597 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityLongPressTimer.java b/java/src/com/android/inputmethod/accessibility/AccessibilityLongPressTimer.java new file mode 100644 index 000000000..37d910edb --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityLongPressTimer.java @@ -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 performLongClickOn(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.performLongClickOn((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); + } +} diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java index 10fb9fef4..2762a9f25 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java @@ -17,7 +17,6 @@ package com.android.inputmethod.accessibility; import android.content.Context; -import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.os.Build; import android.os.SystemClock; @@ -63,13 +62,11 @@ public final class AccessibilityUtils { */ private static final boolean ENABLE_ACCESSIBILITY = true; - public static void init(final InputMethodService inputMethod) { + public static void init(final Context context) { if (!ENABLE_ACCESSIBILITY) return; // These only need to be initialized if the kill switch is off. - sInstance.initInternal(inputMethod); - KeyCodeDescriptionMapper.init(); - AccessibleKeyboardViewProxy.init(inputMethod); + sInstance.initInternal(context); } public static AccessibilityUtils getInstance() { @@ -116,7 +113,7 @@ public final class AccessibilityUtils { * @param event The event to check. * @return {@true} is the event is a touch exploration event */ - public boolean isTouchExplorationEvent(final MotionEvent event) { + public static boolean isTouchExplorationEvent(final MotionEvent event) { final int action = event.getAction(); return action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_EXIT @@ -158,7 +155,7 @@ public final class AccessibilityUtils { * @param typedWord the currently typed word */ public void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) { - if (suggestedWords != null && suggestedWords.mWillAutoCorrect) { + if (suggestedWords.mWillAutoCorrect) { mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); mTypedWord = typedWord; } else { diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java deleted file mode 100644 index 73896dfd3..000000000 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java +++ /dev/null @@ -1,411 +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.Context; -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.util.SparseIntArray; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewParent; -import android.view.accessibility.AccessibilityEvent; - -import com.android.inputmethod.keyboard.Key; -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; - -public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { - private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy(); - - /** Map of keyboard modes to resource IDs. */ - private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray(); - - static { - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url); - } - - private InputMethodService mInputMethod; - private MainKeyboardView mView; - private AccessibilityEntityProvider mAccessibilityNodeProvider; - - private Key mLastHoverKey = null; - - /** - * Inset in pixels to look for keys when the user's finger exits the keyboard area. - */ - private int mEdgeSlop; - - /** The most recently set keyboard mode. */ - private int mLastKeyboardMode; - - public static void init(final InputMethodService inputMethod) { - sInstance.initInternal(inputMethod); - } - - public static AccessibleKeyboardViewProxy getInstance() { - return sInstance; - } - - private AccessibleKeyboardViewProxy() { - // Not publicly instantiable. - } - - private void initInternal(final InputMethodService inputMethod) { - mInputMethod = inputMethod; - mEdgeSlop = inputMethod.getResources().getDimensionPixelSize( - R.dimen.accessibility_edge_slop); - } - - /** - * Sets the view wrapped by this proxy. - * - * @param view The view to wrap. - */ - public void setView(final MainKeyboardView view) { - if (view == null) { - // Ignore null views. - return; - } - mView = view; - - // Ensure that the view has an accessibility delegate. - ViewCompat.setAccessibilityDelegate(view, this); - - if (mAccessibilityNodeProvider == null) { - return; - } - mAccessibilityNodeProvider.setView(view); - } - - /** - * Called when the keyboard layout changes. - * <p> - * <b>Note:</b> This method will be called even if accessibility is not - * enabled. - */ - public void setKeyboard() { - if (mView == null) { - return; - } - if (mAccessibilityNodeProvider != null) { - mAccessibilityNodeProvider.setKeyboard(); - } - final int keyboardMode = mView.getKeyboard().mId.mMode; - - // Since this method is called even when accessibility is off, make sure - // to check the state before announcing anything. Also, don't announce - // changes within the same mode. - if (AccessibilityUtils.getInstance().isAccessibilityEnabled() - && (mLastKeyboardMode != keyboardMode)) { - announceKeyboardMode(keyboardMode); - } - mLastKeyboardMode = keyboardMode; - } - - /** - * Called when the keyboard is hidden and accessibility is enabled. - */ - public void onHideWindow() { - if (mView == null) { - return; - } - announceKeyboardHidden(); - mLastKeyboardMode = -1; - } - - /** - * Announces which type of keyboard is being displayed. If the keyboard type - * is unknown, no announcement is made. - * - * @param mode The new keyboard mode. - */ - private void announceKeyboardMode(int mode) { - final int resId = KEYBOARD_MODE_RES_IDS.get(mode); - if (resId == 0) { - return; - } - final Context context = mView.getContext(); - final String keyboardMode = context.getString(resId); - final String text = context.getString(R.string.announce_keyboard_mode, keyboardMode); - sendWindowStateChanged(text); - } - - /** - * Announces that the keyboard has been hidden. - */ - private void announceKeyboardHidden() { - final Context context = mView.getContext(); - final String text = context.getString(R.string.announce_keyboard_hidden); - - sendWindowStateChanged(text); - } - - /** - * Sends a window state change event with the specified text. - * - * @param text The text to send with the event. - */ - private void sendWindowStateChanged(final String text) { - final AccessibilityEvent stateChange = AccessibilityEvent.obtain( - AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - mView.onInitializeAccessibilityEvent(stateChange); - stateChange.getText().add(text); - stateChange.setContentDescription(null); - - final ViewParent parent = mView.getParent(); - if (parent != null) { - parent.requestSendAccessibilityEvent(mView, stateChange); - } - } - - /** - * Proxy method for View.getAccessibilityNodeProvider(). This method is called in SDK - * version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual - * node hierarchy provider. - * - * @param host The host view for the provider. - * @return The accessibility node provider for the current keyboard. - */ - @Override - public AccessibilityEntityProvider getAccessibilityNodeProvider(final View host) { - if (mView == null) { - return null; - } - return getAccessibilityNodeProvider(); - } - - /** - * Intercepts touch events before dispatch when touch exploration is turned on in ICS and - * higher. - * - * @param event The motion event being dispatched. - * @return {@code true} if the event is handled - */ - public boolean dispatchTouchEvent(final MotionEvent event) { - // To avoid accidental key presses during touch exploration, always drop - // touch events generated by the user. - return false; - } - - /** - * Receives hover events when touch exploration is turned on in SDK versions ICS and higher. - * - * @param event The hover event. - * @return {@code true} if the event is handled - */ - public boolean dispatchHoverEvent(final MotionEvent event, final PointerTracker tracker) { - if (mView == null) { - return false; - } - - final int x = (int) event.getX(); - final int y = (int) event.getY(); - final Key previousKey = mLastHoverKey; - final Key key; - - if (pointInView(x, y)) { - key = tracker.getKeyOn(x, y); - } else { - key = null; - } - mLastHoverKey = key; - - switch (event.getAction()) { - 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 (key != null) { - getAccessibilityNodeProvider().simulateKeyPress(key); - } - //$FALL-THROUGH$ - case MotionEvent.ACTION_HOVER_ENTER: - return onHoverKey(key, event); - case MotionEvent.ACTION_HOVER_MOVE: - if (key != previousKey) { - return onTransitionKey(key, previousKey, event); - } - return onHoverKey(key, event); - } - return false; - } - - /** - * @return A lazily-instantiated node provider for this view proxy. - */ - private AccessibilityEntityProvider getAccessibilityNodeProvider() { - // Instantiate the provide only when requested. Since the system - // will call this method multiple times it is a good practice to - // cache the provider instance. - if (mAccessibilityNodeProvider == null) { - mAccessibilityNodeProvider = new AccessibilityEntityProvider(mView, mInputMethod); - } - return mAccessibilityNodeProvider; - } - - /** - * Utility method to determine whether the given point, in local 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(final int localX, final 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(final Key currentKey, final Key previousKey, - final 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(final Key key, final MotionEvent event) { - // Null keys can't receive events. - if (key == null) { - return false; - } - final AccessibilityEntityProvider provider = getAccessibilityNodeProvider(); - - 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; - } - return true; - } - - /** - * Notifies the user of changes in the keyboard shift state. - */ - public void notifyShiftState() { - if (mView == null) { - return; - } - - 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().announceForAccessibility(mView, text); - } - - /** - * Notifies the user of changes in the keyboard symbols state. - */ - public void notifySymbolsState() { - if (mView == null) { - return; - } - - 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().announceForAccessibility(mView, text); - } -} diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java index 58624a2e6..7a3510ee1 100644 --- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java +++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java @@ -17,6 +17,7 @@ package com.android.inputmethod.accessibility; import android.content.Context; +import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; @@ -27,40 +28,29 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.StringUtils; -import java.util.HashMap; +import java.util.Locale; -public final class KeyCodeDescriptionMapper { +final class KeyCodeDescriptionMapper { private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName(); + private static final String SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X"; + private static final String SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X"; + private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X"; // 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 - private final HashMap<CharSequence, Integer> mKeyLabelMap = CollectionUtils.newHashMap(); - - // Sparse array of spoken description resource IDs indexed by key codes - private final SparseIntArray mKeyCodeMap; - - public static void init() { - sInstance.initInternal(); - } + private static final KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper(); public static KeyCodeDescriptionMapper getInstance() { return sInstance; } - private KeyCodeDescriptionMapper() { - mKeyCodeMap = new SparseIntArray(); - } - - private void initInternal() { - // Manual label substitutions for key labels with no string resource - mKeyLabelMap.put(":-)", R.string.spoken_description_smiley); + // Sparse array of spoken description resource IDs indexed by key codes + private final SparseIntArray mKeyCodeMap = new SparseIntArray(); + private KeyCodeDescriptionMapper() { // Special non-character codes defined in Keyboard mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space); mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete); @@ -75,19 +65,21 @@ public final class KeyCodeDescriptionMapper { mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next); mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS, R.string.spoken_description_action_previous); + mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji); + // Because the upper-case and lower-case mappings of the following letters is depending on + // the locale, the upper case descriptions should be defined here. The lower case + // descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}. + // U+0049: "I" LATIN CAPITAL LETTER I + // U+0069: "i" LATIN SMALL LETTER I + // U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + mKeyCodeMap.put(0x0049, R.string.spoken_letter_0049); + mKeyCodeMap.put(0x0130, R.string.spoken_letter_0130); } /** * Returns the localized description of the action performed by a specified * key based on the current keyboard state. - * <p> - * The order of precedence for key descriptions is: - * <ol> - * <li>Manually-defined based on the key label</li> - * <li>Automatic or manually-defined based on the key code</li> - * <li>Automatically based on the key label</li> - * <li>{code null} for keys with no label or key code defined</li> - * </p> * * @param context The package's context. * @param keyboard The keyboard on which the key resides. @@ -116,18 +108,26 @@ public final class KeyCodeDescriptionMapper { return getDescriptionForActionKey(context, keyboard, key); } - if (!TextUtils.isEmpty(key.getLabel())) { - final String label = key.getLabel().trim(); - - // First, attempt to map the label to a pre-defined description. - if (mKeyLabelMap.containsKey(label)) { - return context.getString(mKeyLabelMap.get(label)); - } + if (code == Constants.CODE_OUTPUT_TEXT) { + return key.getOutputText(); } // Just attempt to speak the description. - if (key.getCode() != Constants.CODE_UNSPECIFIED) { - return getDescriptionForKeyCode(context, keyboard, key, shouldObscure); + if (code != Constants.CODE_UNSPECIFIED) { + // 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); + } + final String description = getDescriptionForCodePoint(context, code); + if (description != null) { + return description; + } + if (!TextUtils.isEmpty(key.getLabel())) { + return key.getLabel(); + } + return context.getString(R.string.spoken_description_unknown); } return null; } @@ -141,7 +141,7 @@ public final class KeyCodeDescriptionMapper { * @param keyboard The keyboard on which the key resides. * @return a character sequence describing the action performed by pressing the key */ - private String getDescriptionForSwitchAlphaSymbol(final Context context, + private static String getDescriptionForSwitchAlphaSymbol(final Context context, final Keyboard keyboard) { final KeyboardId keyboardId = keyboard.mId; final int elementId = keyboardId.mElementId; @@ -179,7 +179,8 @@ public final class KeyCodeDescriptionMapper { * @param keyboard The keyboard on which the key resides. * @return A context-sensitive description of the "Shift" key. */ - private String getDescriptionForShiftKey(final Context context, final Keyboard keyboard) { + private static String getDescriptionForShiftKey(final Context context, + final Keyboard keyboard) { final KeyboardId keyboardId = keyboard.mId; final int elementId = keyboardId.mElementId; final int resId; @@ -191,9 +192,14 @@ public final class KeyCodeDescriptionMapper { 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; + case KeyboardId.ELEMENT_SYMBOLS: + resId = R.string.spoken_description_symbols_shift; + break; + case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: + resId = R.string.spoken_description_symbols_shift_shifted; + break; default: resId = R.string.spoken_description_shift; } @@ -208,7 +214,7 @@ public final class KeyCodeDescriptionMapper { * @param key The key to describe. * @return Returns a context-sensitive description of the "Enter" action key. */ - private String getDescriptionForActionKey(final Context context, final Keyboard keyboard, + private static String getDescriptionForActionKey(final Context context, final Keyboard keyboard, final Key key) { final KeyboardId keyboardId = keyboard.mId; final int actionId = keyboardId.imeAction(); @@ -247,42 +253,91 @@ public final class KeyCodeDescriptionMapper { /** * Returns a localized character sequence describing what will happen when - * the specified key is pressed based on its key code. - * <p> - * The order of precedence for key code descriptions is: - * <ol> - * <li>Manually-defined shift-locked description</li> - * <li>Manually-defined shifted description</li> - * <li>Manually-defined normal description</li> - * <li>Automatic based on the character represented by the key code</li> - * <li>Fall-back for undefined or control characters</li> - * </ol> - * </p> + * the specified key is pressed based on its key code point. * * @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 + * @param codePoint The code point from which to obtain a description. + * @return a character sequence describing the code point. */ - private String getDescriptionForKeyCode(final Context context, final Keyboard keyboard, - final Key key, final boolean shouldObscure) { - final int code = key.getCode(); - + public String getDescriptionForCodePoint(final Context context, final int codePoint) { // 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); + final int index = mKeyCodeMap.indexOfKey(codePoint); + if (index >= 0) { + return context.getString(mKeyCodeMap.valueAt(index)); } - if (mKeyCodeMap.indexOfKey(code) >= 0) { - return context.getString(mKeyCodeMap.get(code)); + final String accentedLetter = getSpokenAccentedLetterDescription(context, codePoint); + if (accentedLetter != null) { + return accentedLetter; } - if (isDefinedNonCtrl) { - return Character.toString((char) code); + // Here, <code>code</code> may be a base (non-accented) letter. + final String unsupportedSymbol = getSpokenSymbolDescription(context, codePoint); + if (unsupportedSymbol != null) { + return unsupportedSymbol; } - if (!TextUtils.isEmpty(key.getLabel())) { - return key.getLabel(); + final String emojiDescription = getSpokenEmojiDescription(context, codePoint); + if (emojiDescription != null) { + return emojiDescription; + } + if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) { + return StringUtils.newSingleCodePointString(codePoint); + } + return null; + } + + // TODO: Remove this method once TTS supports those accented letters' verbalization. + private String getSpokenAccentedLetterDescription(final Context context, final int code) { + final boolean isUpperCase = Character.isUpperCase(code); + final int baseCode = isUpperCase ? Character.toLowerCase(code) : code; + final int baseIndex = mKeyCodeMap.indexOfKey(baseCode); + final int resId = (baseIndex >= 0) ? mKeyCodeMap.valueAt(baseIndex) + : getSpokenDescriptionId(context, baseCode, SPOKEN_LETTER_RESOURCE_NAME_FORMAT); + if (resId == 0) { + return null; + } + final String spokenText = context.getString(resId); + return isUpperCase ? context.getString(R.string.spoken_description_upper_case, spokenText) + : spokenText; + } + + // TODO: Remove this method once TTS supports those symbols' verbalization. + private String getSpokenSymbolDescription(final Context context, final int code) { + final int resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT); + if (resId == 0) { + return null; + } + final String spokenText = context.getString(resId); + if (!TextUtils.isEmpty(spokenText)) { + return spokenText; + } + // If a translated description is empty, fall back to unknown symbol description. + return context.getString(R.string.spoken_symbol_unknown); + } + + // TODO: Remove this method once TTS supports emoji verbalization. + private String getSpokenEmojiDescription(final Context context, final int code) { + final int resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT); + if (resId == 0) { + return null; + } + final String spokenText = context.getString(resId); + if (!TextUtils.isEmpty(spokenText)) { + return spokenText; + } + // If a translated description is empty, fall back to unknown emoji description. + return context.getString(R.string.spoken_emoji_unknown); + } + + private int getSpokenDescriptionId(final Context context, final int code, + final String resourceNameFormat) { + final String resourceName = String.format(Locale.ROOT, resourceNameFormat, code); + final Resources resources = context.getResources(); + // Note that the resource package name may differ from the context package name. + final String resourcePackageName = resources.getResourcePackageName( + R.string.spoken_description_unknown); + final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName); + if (resId != 0) { + mKeyCodeMap.append(code, resId); } - return context.getString(R.string.spoken_description_unknown, code); + return resId; } } diff --git a/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java new file mode 100644 index 000000000..237117d10 --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java @@ -0,0 +1,327 @@ +/* + * 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.Context; +import android.os.SystemClock; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.KeyDetector; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardView; + +/** + * 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<KV> mAccessibilityNodeProvider; + private Key mLastHoverKey; + + public static final int HOVER_EVENT_POINTER_ID = 0; + + public KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector) { + super(); + mKeyboardView = keyboardView; + mKeyDetector = keyDetector; + + // Ensure that the view has an accessibility delegate. + ViewCompat.setAccessibilityDelegate(keyboardView, this); + } + + /** + * Called when the keyboard layout changes. + * <p> + * <b>Note:</b> This method will be called even if accessibility is not + * enabled. + * @param keyboard The keyboard that is being set to the wrapping view. + */ + public void setKeyboard(final Keyboard keyboard) { + if (keyboard == null) { + return; + } + if (mAccessibilityNodeProvider != null) { + mAccessibilityNodeProvider.setKeyboard(keyboard); + } + mKeyboard = keyboard; + } + + protected final Keyboard getKeyboard() { + return mKeyboard; + } + + protected final void setLastHoverKey(final Key key) { + mLastHoverKey = key; + } + + protected final Key getLastHoverKey() { + return mLastHoverKey; + } + + /** + * Sends a window state change event with the specified string resource id. + * + * @param resId The string resource id of the text to send with the event. + */ + protected void sendWindowStateChanged(final int resId) { + if (resId == 0) { + return; + } + final Context context = mKeyboardView.getContext(); + sendWindowStateChanged(context.getString(resId)); + } + + /** + * Sends a window state change event with the specified text. + * + * @param text The text to send with the event. + */ + protected void sendWindowStateChanged(final String text) { + final AccessibilityEvent stateChange = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + mKeyboardView.onInitializeAccessibilityEvent(stateChange); + stateChange.getText().add(text); + stateChange.setContentDescription(null); + + final ViewParent parent = mKeyboardView.getParent(); + if (parent != null) { + parent.requestSendAccessibilityEvent(mKeyboardView, stateChange); + } + } + + /** + * Delegate method for View.getAccessibilityNodeProvider(). This method is called in SDK + * version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual + * node hierarchy provider. + * + * @param host The host view for the provider. + * @return The accessibility node provider for the current keyboard. + */ + @Override + public KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider(final View host) { + return getAccessibilityNodeProvider(); + } + + /** + * @return A lazily-instantiated node provider for this view delegate. + */ + protected KeyboardAccessibilityNodeProvider<KV> 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 KeyboardAccessibilityNodeProvider<>(mKeyboardView, this); + } + return mAccessibilityNodeProvider; + } + + /** + * Get a key that a hover event is on. + * + * @param event The hover event. + * @return key The key that the <code>event</code> is on. + */ + protected final Key getHoverKeyOf(final MotionEvent event) { + final int actionIndex = event.getActionIndex(); + final int x = (int)event.getX(actionIndex); + final int y = (int)event.getY(actionIndex); + return mKeyDetector.detectHitKey(x, y); + } + + /** + * Receives hover events when touch exploration is turned on in SDK versions ICS and higher. + * + * @param event The hover event. + * @return {@code true} if the event is handled. + */ + public boolean onHoverEvent(final MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_HOVER_ENTER: + onHoverEnter(event); + break; + case MotionEvent.ACTION_HOVER_MOVE: + onHoverMove(event); + break; + case MotionEvent.ACTION_HOVER_EXIT: + onHoverExit(event); + break; + default: + Log.w(getClass().getSimpleName(), "Unknown hover event: " + event); + break; + } + return true; + } + + /** + * Process {@link MotionEvent#ACTION_HOVER_ENTER} event. + * + * @param event A hover enter event. + */ + protected void onHoverEnter(final MotionEvent event) { + final Key key = getHoverKeyOf(event); + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverEnter: key=" + key); + } + if (key != null) { + onHoverEnterTo(key); + } + setLastHoverKey(key); + } + + /** + * Process {@link MotionEvent#ACTION_HOVER_MOVE} event. + * + * @param event A hover move event. + */ + protected void onHoverMove(final MotionEvent event) { + final Key lastKey = getLastHoverKey(); + final Key key = getHoverKeyOf(event); + if (key != lastKey) { + if (lastKey != null) { + onHoverExitFrom(lastKey); + } + if (key != null) { + onHoverEnterTo(key); + } + } + if (key != null) { + onHoverMoveWithin(key); + } + setLastHoverKey(key); + } + + /** + * Process {@link MotionEvent#ACTION_HOVER_EXIT} event. + * + * @param event A hover exit event. + */ + 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) { + 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) { + performClickOn(key); + onHoverExitFrom(key); + } + setLastHoverKey(null); + } + + /** + * Perform click on a key. + * + * @param key A key to be registered. + */ + public void performClickOn(final Key key) { + if (DEBUG_HOVER) { + Log.d(TAG, "performClickOn: key=" + key); + } + simulateTouchEvent(MotionEvent.ACTION_DOWN, key); + simulateTouchEvent(MotionEvent.ACTION_UP, key); + } + + /** + * Simulating a touch event by injecting a synthesized touch event into {@link KeyboardView}. + * + * @param touchAction The action of the synthesizing touch event. + * @param key The key that a synthesized touch event is on. + */ + private void simulateTouchEvent(final int touchAction, final Key key) { + final int x = key.getHitBox().centerX(); + final int y = key.getHitBox().centerY(); + final long eventTime = SystemClock.uptimeMillis(); + final MotionEvent touchEvent = MotionEvent.obtain( + eventTime, eventTime, touchAction, x, y, 0 /* metaState */); + mKeyboardView.onTouchEvent(touchEvent); + touchEvent.recycle(); + } + + /** + * Handles a hover enter event on a key. + * + * @param key The currently hovered key. + */ + protected void onHoverEnterTo(final Key key) { + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverEnterTo: key=" + key); + } + key.onPressed(); + mKeyboardView.invalidateKey(key); + final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider(); + provider.onHoverEnterTo(key); + provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); + } + + /** + * Handles a hover move event on a key. + * + * @param key The currently hovered key. + */ + protected void onHoverMoveWithin(final Key key) { } + + /** + * Handles a hover exit event on a key. + * + * @param key The currently hovered key. + */ + protected void onHoverExitFrom(final Key key) { + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverExitFrom: key=" + key); + } + key.onReleased(); + mKeyboardView.invalidateKey(key); + final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider(); + provider.onHoverExitFrom(key); + } + + /** + * Perform long click on a key. + * + * @param key A key to be long pressed on. + */ + public void performLongClickOn(final Key key) { + // A extended class should override this method to implement long press. + } +} diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java index c628c5b09..66b0acb2f 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java +++ b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java @@ -17,17 +17,13 @@ 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; @@ -37,9 +33,10 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardView; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsValues; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; +import java.util.List; + /** * Exposes a virtual view sub-tree for {@link KeyboardView} and generates * {@link AccessibilityEvent}s for individual {@link Key}s. @@ -50,17 +47,16 @@ import com.android.inputmethod.latin.utils.CoordinateUtils; * virtual views, thus conveying their logical structure. * </p> */ -public final class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat { - private static final String TAG = AccessibilityEntityProvider.class.getSimpleName(); - private static final int UNDEFINED = Integer.MIN_VALUE; +final class KeyboardAccessibilityNodeProvider<KV extends KeyboardView> + extends AccessibilityNodeProviderCompat { + private static final String TAG = KeyboardAccessibilityNodeProvider.class.getSimpleName(); + + // From {@link android.view.accessibility.AccessibilityNodeInfo#UNDEFINED_ITEM_ID}. + private static final int UNDEFINED = Integer.MAX_VALUE; - private final InputMethodService mInputMethodService; private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper; private final AccessibilityUtils mAccessibilityUtils; - /** A map of integer IDs to {@link Key}s. */ - private final SparseArray<Key> mVirtualViewIdToKey = CollectionUtils.newSparseArray(); - /** Temporary rect used to calculate in-screen bounds. */ private final Rect mTempBoundsInScreen = new Rect(); @@ -70,36 +66,64 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider /** The virtual view identifier for the focused node. */ private int mAccessibilityFocusedView = UNDEFINED; - /** The current keyboard view. */ - private KeyboardView mKeyboardView; + /** The virtual view identifier for the hovering node. */ + private int mHoveringNodeId = UNDEFINED; + + /** The keyboard view to provide an accessibility node info. */ + private final KV mKeyboardView; + /** The accessibility delegate. */ + private final KeyboardAccessibilityDelegate<KV> mDelegate; - public AccessibilityEntityProvider(final KeyboardView keyboardView, - final InputMethodService inputMethod) { - mInputMethodService = inputMethod; + /** The current keyboard. */ + private Keyboard mKeyboard; + + public KeyboardAccessibilityNodeProvider(final KV keyboardView, + final KeyboardAccessibilityDelegate<KV> delegate) { + super(); 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(final KeyboardView keyboardView) { mKeyboardView = keyboardView; - updateParentLocation(); + mDelegate = delegate; // Since this class is constructed lazily, we might not get a subsequent // call to setKeyboard() and therefore need to call it now. - setKeyboard(); + setKeyboard(keyboardView.getKeyboard()); } /** * Sets the keyboard represented by this node provider. + * + * @param keyboard The keyboard that is being set to the keyboard view. */ - public void setKeyboard() { - assignVirtualViewIds(); + public void setKeyboard(final Keyboard keyboard) { + mKeyboard = keyboard; + } + + private Key getKeyOf(final int virtualViewId) { + if (mKeyboard == null) { + return null; + } + final List<Key> sortedKeys = mKeyboard.getSortedKeys(); + // Use a virtual view id as an index of the sorted keys list. + if (virtualViewId >= 0 && virtualViewId < sortedKeys.size()) { + return sortedKeys.get(virtualViewId); + } + return null; + } + + private int getVirtualViewIdOf(final Key key) { + if (mKeyboard == null) { + return View.NO_ID; + } + final List<Key> sortedKeys = mKeyboard.getSortedKeys(); + final int size = sortedKeys.size(); + for (int index = 0; index < size; index++) { + if (sortedKeys.get(index) == key) { + // Use an index of the sorted keys list as a virtual view id. + return index; + } + } + return View.NO_ID; } /** @@ -112,18 +136,40 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider * @see AccessibilityEvent */ public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) { - final int virtualViewId = generateVirtualViewIdForKey(key); + final int virtualViewId = getVirtualViewIdOf(key); final String keyDescription = getKeyDescription(key); final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); event.setPackageName(mKeyboardView.getContext().getPackageName()); event.setClassName(key.getClass().getName()); event.setContentDescription(keyDescription); event.setEnabled(true); - final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event); + final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); record.setSource(mKeyboardView, virtualViewId); return event; } + public void onHoverEnterTo(final Key key) { + final int id = getVirtualViewIdOf(key); + if (id == View.NO_ID) { + return; + } + // Start hovering on the key. Because our accessibility model is lift-to-type, we should + // report the node info without click and long click actions to avoid unnecessary + // announcements. + mHoveringNodeId = id; + // Invalidate the node info of the key. + sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); + sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER); + } + + public void onHoverExitFrom(final Key key) { + mHoveringNodeId = UNDEFINED; + // Invalidate the node info of the key to be able to revert the change we have done + // in {@link #onHoverEnterTo(Key)}. + sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); + sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT); + } + /** * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or @@ -156,19 +202,24 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider final AccessibilityNodeInfoCompat rootInfo = AccessibilityNodeInfoCompat.obtain(mKeyboardView); ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo); + updateParentLocation(); // Add the virtual children of the root View. - final Keyboard keyboard = mKeyboardView.getKeyboard(); - final Key[] keys = keyboard.getKeys(); - for (Key key : keys) { - final int childVirtualViewId = generateVirtualViewIdForKey(key); - rootInfo.addChild(mKeyboardView, childVirtualViewId); + final List<Key> sortedKeys = mKeyboard.getSortedKeys(); + final int size = sortedKeys.size(); + for (int index = 0; index < size; index++) { + final Key key = sortedKeys.get(index); + if (key.isSpacer()) { + continue; + } + // Use an index of the sorted keys list as a virtual view id. + rootInfo.addChild(mKeyboardView, index); } return rootInfo; } - // Find the view that corresponds to the given id. - final Key key = mVirtualViewIdToKey.get(virtualViewId); + // Find the key that corresponds to the given virtual view id. + final Key key = getKeyOf(virtualViewId); if (key == null) { Log.e(TAG, "Invalid virtual view ID: " + virtualViewId); return null; @@ -191,9 +242,16 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider info.setBoundsInScreen(boundsInScreen); info.setParent(mKeyboardView); info.setSource(mKeyboardView, virtualViewId); - info.setBoundsInScreen(boundsInScreen); - info.setEnabled(true); + info.setEnabled(key.isEnabled()); info.setVisibleToUser(true); + // Don't add ACTION_CLICK and ACTION_LONG_CLOCK actions while hovering on the key. + // See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}. + if (virtualViewId != mHoveringNodeId) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); + if (key.isLongPressEnabled()) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK); + } + } if (mAccessibilityFocusedView == virtualViewId) { info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); @@ -203,35 +261,14 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider 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(final Key key) { - final int x = key.getHitBox().centerX(); - final int y = key.getHitBox().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); - downEvent.recycle(); - upEvent.recycle(); - } - @Override public boolean performAction(final int virtualViewId, final int action, final Bundle arguments) { - final Key key = mVirtualViewIdToKey.get(virtualViewId); + final Key key = getKeyOf(virtualViewId); if (key == null) { return false; } - return performActionForKey(key, action, arguments); + return performActionForKey(key, action); } /** @@ -239,29 +276,28 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider * * @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(final Key key, final int action, final Bundle arguments) { - final int virtualViewId = generateVirtualViewIdForKey(key); - + boolean performActionForKey(final Key key, final int action) { switch (action) { case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: - if (mAccessibilityFocusedView == virtualViewId) { - return false; - } - mAccessibilityFocusedView = virtualViewId; + mAccessibilityFocusedView = getVirtualViewIdOf(key); 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; + case AccessibilityNodeInfoCompat.ACTION_CLICK: + sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_CLICKED); + mDelegate.performClickOn(key); + return true; + case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK: + sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); + mDelegate.performLongClickOn(key); + return true; default: return false; } @@ -285,11 +321,11 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider * @return The context-specific description of the key. */ private String getKeyDescription(final Key key) { - final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo(); + final EditorInfo editorInfo = mKeyboard.mId.mEditorInfo; final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo); final SettingsValues currentSettings = Settings.getInstance().getCurrent(); final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey( - mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure); + mKeyboardView.getContext(), mKeyboard, key, shouldObscure); if (currentSettings.isWordSeparator(key.getCode())) { return mAccessibilityUtils.getAutoCorrectionDescription( keyCodeDescription, shouldObscure); @@ -299,40 +335,9 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider } /** - * 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.getKeys(); - 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(final 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.getX()) << (Integer.SIZE / 2)) | (0xFFFF & key.getY()); - } } diff --git a/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java new file mode 100644 index 000000000..b84d402fb --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java @@ -0,0 +1,303 @@ +/* + * 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.graphics.Rect; +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> + 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(); + + static { + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url); + } + + /** The most recently set keyboard mode. */ + private int mLastKeyboardMode = KEYBOARD_IS_HIDDEN; + private static final int KEYBOARD_IS_HIDDEN = -1; + // The rectangle region to ignore hover events. + private final Rect mBoundsToIgnoreHoverEvent = new Rect(); + + private final AccessibilityLongPressTimer mAccessibilityLongPressTimer; + + public MainKeyboardAccessibilityDelegate(final MainKeyboardView mainKeyboardView, + final KeyDetector keyDetector) { + super(mainKeyboardView, keyDetector); + mAccessibilityLongPressTimer = new AccessibilityLongPressTimer( + this /* callback */, mainKeyboardView.getContext()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setKeyboard(final Keyboard keyboard) { + if (keyboard == null) { + return; + } + final Keyboard lastKeyboard = getKeyboard(); + super.setKeyboard(keyboard); + final int lastKeyboardMode = mLastKeyboardMode; + mLastKeyboardMode = keyboard.mId.mMode; + + // Since this method is called even when accessibility is off, make sure + // to check the state before announcing anything. + if (!AccessibilityUtils.getInstance().isAccessibilityEnabled()) { + return; + } + // Announce the language name only when the language is changed. + if (lastKeyboard == null || !keyboard.mId.mSubtype.equals(lastKeyboard.mId.mSubtype)) { + announceKeyboardLanguage(keyboard); + return; + } + // Announce the mode only when the mode is changed. + if (keyboard.mId.mMode != lastKeyboardMode) { + announceKeyboardMode(keyboard); + return; + } + // Announce the keyboard type only when the type is changed. + if (keyboard.mId.mElementId != lastKeyboard.mId.mElementId) { + announceKeyboardType(keyboard, lastKeyboard); + return; + } + } + + /** + * Called when the keyboard is hidden and accessibility is enabled. + */ + public void onHideWindow() { + announceKeyboardHidden(); + mLastKeyboardMode = KEYBOARD_IS_HIDDEN; + } + + /** + * Announces which language of keyboard is being displayed. + * + * @param keyboard The new keyboard. + */ + private void announceKeyboardLanguage(final Keyboard keyboard) { + final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale( + keyboard.mId.mSubtype); + sendWindowStateChanged(languageText); + } + + /** + * Announces which type of keyboard is being displayed. + * If the keyboard type is unknown, no announcement is made. + * + * @param keyboard The new keyboard. + */ + private void announceKeyboardMode(final Keyboard keyboard) { + final Context context = mKeyboardView.getContext(); + final int modeTextResId = KEYBOARD_MODE_RES_IDS.get(keyboard.mId.mMode); + if (modeTextResId == 0) { + return; + } + final String modeText = context.getString(modeTextResId); + final String text = context.getString(R.string.announce_keyboard_mode, modeText); + sendWindowStateChanged(text); + } + + /** + * Announces which type of keyboard is being displayed. + * + * @param keyboard The new keyboard. + * @param lastKeyboard The last keyboard. + */ + private void announceKeyboardType(final Keyboard keyboard, final Keyboard lastKeyboard) { + final int lastElementId = lastKeyboard.mId.mElementId; + final int resId; + switch (keyboard.mId.mElementId) { + case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET: + if (lastElementId == KeyboardId.ELEMENT_ALPHABET + || lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) { + // Transition between alphabet mode and automatic shifted mode should be silently + // ignored because it can be determined by each key's talk back announce. + return; + } + resId = R.string.spoken_description_mode_alpha; + break; + case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: + if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) { + // Resetting automatic shifted mode by pressing the shift key causes the transition + // from automatic shifted to manual shifted that should be silently ignored. + return; + } + resId = R.string.spoken_description_shiftmode_on; + break; + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: + if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) { + // Resetting caps locked mode by pressing the shift key causes the transition + // from shift locked to shift lock shifted that should be silently ignored. + return; + } + resId = R.string.spoken_description_shiftmode_locked; + break; + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: + resId = R.string.spoken_description_shiftmode_locked; + break; + case KeyboardId.ELEMENT_SYMBOLS: + resId = R.string.spoken_description_mode_symbol; + break; + case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: + resId = R.string.spoken_description_mode_symbol_shift; + 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: + return; + } + sendWindowStateChanged(resId); + } + + /** + * Announces that the keyboard has been hidden. + */ + private void announceKeyboardHidden() { + sendWindowStateChanged(R.string.announce_keyboard_hidden); + } + + @Override + public void performClickOn(final Key key) { + final int x = key.getHitBox().centerX(); + final int y = key.getHitBox().centerY(); + if (DEBUG_HOVER) { + Log.d(TAG, "performClickOn: key=" + key + + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y)); + } + if (mBoundsToIgnoreHoverEvent.contains(x, y)) { + // This hover exit event points to the key that should be ignored. + // Clear the ignoring region to handle further hover events. + mBoundsToIgnoreHoverEvent.setEmpty(); + return; + } + super.performClickOn(key); + } + + @Override + protected void onHoverEnterTo(final Key key) { + final int x = key.getHitBox().centerX(); + final int y = key.getHitBox().centerY(); + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverEnterTo: key=" + key + + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y)); + } + mAccessibilityLongPressTimer.cancelLongPress(); + if (mBoundsToIgnoreHoverEvent.contains(x, y)) { + return; + } + // This hover enter event points to the key that isn't in the ignoring region. + // Further hover events should be handled. + mBoundsToIgnoreHoverEvent.setEmpty(); + super.onHoverEnterTo(key); + if (key.isLongPressEnabled()) { + mAccessibilityLongPressTimer.startLongPress(key); + } + } + + @Override + protected void onHoverExitFrom(final Key key) { + final int x = key.getHitBox().centerX(); + final int y = key.getHitBox().centerY(); + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverExitFrom: key=" + key + + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y)); + } + mAccessibilityLongPressTimer.cancelLongPress(); + super.onHoverExitFrom(key); + } + + @Override + public void performLongClickOn(final Key key) { + if (DEBUG_HOVER) { + Log.d(TAG, "performLongClickOn: 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. + if (tracker.isInOperation()) { + // This long press shows a more keys keyboard and further hover events should be + // handled. + mBoundsToIgnoreHoverEvent.setEmpty(); + return; + } + // This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}. + // We should ignore further hover events on this key. + mBoundsToIgnoreHoverEvent.set(key.getHitBox()); + if (key.hasNoPanelAutoMoreKey()) { + // This long press has registered a code point without showing a more keys keyboard. + // We should talk back the code point if possible. + final int codePointOfNoPanelAutoMoreKey = key.getMoreKeys()[0].mCode; + final String text = KeyCodeDescriptionMapper.getInstance().getDescriptionForCodePoint( + mKeyboardView.getContext(), codePointOfNoPanelAutoMoreKey); + if (text != null) { + sendWindowStateChanged(text); + } + } + } +} diff --git a/java/src/com/android/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java new file mode 100644 index 000000000..4022da343 --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java @@ -0,0 +1,120 @@ +/* + * 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.keyboard.PointerTracker; + +/** + * 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); + } + + public void onDismissMoreKeysKeyboard() { + sendWindowStateChanged(mCloseAnnounceResId); + } + + @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); + // TODO: Should fix this reference. This is a hack to clear the state of + // {@link PointerTracker}. + PointerTracker.dismissAllMoreKeysPanels(); + return; + } + // Close the more keys keyboard. + // TODO: Should fix this reference. This is a hack to clear the state of + // {@link PointerTracker}. + PointerTracker.dismissAllMoreKeysPanels(); + } +} |