Added support for touch exploration to Latin IME.

Bug: 4379983
Change-Id: I97f22e54827c6229054b514801401ffa5b4ed3b8
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index d3f1923..5d7a1f9 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -131,6 +131,81 @@
     <!-- Label for "Wait" key of phone number keyboard.  Must be short to fit on key! [CHAR LIMIT=5]-->
     <string name="label_wait_key">Wait</string>
 
+    <!-- Spoken description for the currently entered text -->
+    <string name="spoken_current_text_is">Current text is "%s"</string>
+    <!-- Spoken description when there is no text entered -->
+    <string name="spoken_no_text_entered">No text entered</string>
+
+    <!-- Spoken description for unknown keyboard keys. -->
+    <string name="spoken_description_unknown">Key code %d</string>
+    <!-- Spoken description for the "Shift" keyboard key. -->
+    <string name="spoken_description_shift">Shift</string>
+    <!-- Spoken description for the "Shift" keyboard key's pressed state. -->
+    <string name="spoken_description_shift_shifted">Shift enabled</string>
+    <!-- Spoken description for the "Shift" keyboard key's pressed state. -->
+    <string name="spoken_description_caps_lock">Caps lock enabled</string>
+    <!-- Spoken description for the "Delete" keyboard key. -->
+    <string name="spoken_description_delete">Delete</string>
+    <!-- Spoken description for the "To Symbol" keyboard key. -->
+    <string name="spoken_description_to_symbol">Symbols</string>
+    <!-- Spoken description for the "To Alpha" keyboard key. -->
+    <string name="spoken_description_to_alpha">Letters</string>
+    <!-- Spoken description for the "To Numbers" keyboard key. -->
+    <string name="spoken_description_to_numeric">Numbers</string>
+    <!-- Spoken description for the "Settings" keyboard key. -->
+    <string name="spoken_description_settings">Settings</string>
+    <!-- Spoken description for the "Tab" keyboard key. -->
+    <string name="spoken_description_tab">Tab</string>
+    <!-- Spoken description for the "Space" keyboard key. -->
+    <string name="spoken_description_space">Space</string>
+    <!-- Spoken description for the "Mic" keyboard key. -->
+    <string name="spoken_description_mic">Voice input</string>
+    <!-- Spoken description for the "Smiley" keyboard key. -->
+    <string name="spoken_description_smiley">Smiley face</string>
+    <!-- Spoken description for the "Return" keyboard key. -->
+    <string name="spoken_description_return">Return</string>
+
+    <!-- Spoken description for the "," keyboard key. -->
+    <string name="spoken_description_comma">Comma</string>
+    <!-- Spoken description for the "." keyboard key. -->
+    <string name="spoken_description_period">Period</string>
+    <!-- Spoken description for the "(" keyboard key. -->
+    <string name="spoken_description_left_parenthesis">Left parenthesis</string>
+    <!-- Spoken description for the ")" keyboard key. -->
+    <string name="spoken_description_right_parenthesis">Right parenthesis</string>
+    <!-- Spoken description for the ":" keyboard key. -->
+    <string name="spoken_description_colon">Colon</string>
+    <!-- Spoken description for the ";" keyboard key. -->
+    <string name="spoken_description_semicolon">Semicolon</string>
+    <!-- Spoken description for the "!" keyboard key. -->
+    <string name="spoken_description_exclamation_mark">Exclamation mark</string>
+    <!-- Spoken description for the "?" keyboard key. -->
+    <string name="spoken_description_question_mark">Question mark</string>
+    <!-- Spoken description for the """ keyboard key. -->
+    <string name="spoken_description_double_quote">Double quote</string>
+    <!-- Spoken description for the "'" keyboard key. -->
+    <string name="spoken_description_single_quote">Single quote</string>
+    <!-- Spoken description for the "•" keyboard key. -->
+    <string name="spoken_description_dot">Dot</string>
+    <!-- Spoken description for the "√" keyboard key. -->
+    <string name="spoken_description_square_root">Square root</string>
+    <!-- Spoken description for the "π" keyboard key. -->
+    <string name="spoken_description_pi">Pi</string>
+    <!-- Spoken description for the "Δ" keyboard key. -->
+    <string name="spoken_description_delta">Delta</string>
+    <!-- Spoken description for the "™" keyboard key. -->
+    <string name="spoken_description_trademark">Trademark</string>
+    <!-- Spoken description for the "℅" keyboard key. -->
+    <string name="spoken_description_care_of">Care of</string>
+    <!-- Spoken description for the "*" keyboard key. -->
+    <string name="spoken_description_star">Star</string>
+    <!-- Spoken description for the "#" keyboard key. -->
+    <string name="spoken_description_pound">Pound</string>
+    <!-- Spoken description for the "…" keyboard key. -->
+    <string name="spoken_description_ellipsis">Ellipsis</string>
+    <!-- Spoken description for the "„" keyboard key. -->
+    <string name="spoken_description_low_double_quote">Low double quote</string>
+
     <!-- Voice related labels -->
 
     <!-- Title of the warning dialog that shows when a user initiates voice input for
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
new file mode 100644
index 0000000..ae614b7
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
@@ -0,0 +1,133 @@
+/*
+ * 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.accessibilityservice.AccessibilityServiceInfo;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.inputmethodservice.InputMethodService;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
+import com.android.inputmethod.compat.AccessibilityManagerCompatWrapper;
+import com.android.inputmethod.compat.MotionEventCompatUtils;
+
+public class AccessibilityUtils {
+    private static final String TAG = AccessibilityUtils.class.getSimpleName();
+    private static final String CLASS = AccessibilityUtils.class.getClass().getName();
+    private static final String PACKAGE = AccessibilityUtils.class.getClass().getPackage()
+            .getName();
+
+    private static final AccessibilityUtils sInstance = new AccessibilityUtils();
+
+    private AccessibilityManager mAccessibilityManager;
+    private AccessibilityManagerCompatWrapper mCompatManager;
+
+    /*
+     * Setting this constant to {@code false} will disable all keyboard
+     * accessibility code, regardless of whether Accessibility is turned on in
+     * the system settings. It should ONLY be used in the event of an emergency.
+     */
+    private static final boolean ENABLE_ACCESSIBILITY = true;
+
+    public static void init(InputMethodService inputMethod, SharedPreferences prefs) {
+        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);
+    }
+
+    public static AccessibilityUtils getInstance() {
+        return sInstance;
+    }
+
+    private AccessibilityUtils() {
+        // This class is not publicly instantiable.
+    }
+
+    private void initInternal(Context context, SharedPreferences prefs) {
+        mAccessibilityManager = (AccessibilityManager) context
+                .getSystemService(Context.ACCESSIBILITY_SERVICE);
+        mCompatManager = new AccessibilityManagerCompatWrapper(mAccessibilityManager);
+    }
+
+    /**
+     * Returns {@code true} if touch exploration is enabled. Currently, this
+     * means that the kill switch is off, the device supports touch exploration,
+     * and a spoken feedback service is turned on.
+     *
+     * @return {@code true} if touch exploration is enabled.
+     */
+    public boolean isTouchExplorationEnabled() {
+        return ENABLE_ACCESSIBILITY
+                && AccessibilityEventCompatUtils.supportsTouchExploration()
+                && mAccessibilityManager.isEnabled()
+                && !mCompatManager.getEnabledAccessibilityServiceList(
+                        AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty();
+    }
+
+    /**
+     * Returns {@true} if the provided event is a touch exploration (e.g. hover)
+     * event. This is used to determine whether the event should be processed by
+     * the touch exploration code within the keyboard.
+     *
+     * @param event The event to check.
+     * @return {@true} is the event is a touch exploration event
+     */
+    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;
+    }
+
+    /**
+     * Sends the specified text to the {@link AccessibilityManager} to be
+     * spoken.
+     *
+     * @param text the text to speak
+     */
+    public void speak(CharSequence text) {
+        if (!mAccessibilityManager.isEnabled()) {
+            Log.e(TAG, "Attempted to speak when accessibility was disabled!");
+            return;
+        }
+
+        // The following is a hack to avoid using the heavy-weight TextToSpeech
+        // 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);
+
+        event.setPackageName(PACKAGE);
+        event.setClassName(CLASS);
+        event.setEventTime(SystemClock.uptimeMillis());
+        event.setEnabled(true);
+        event.getText().add(text);
+
+        mAccessibilityManager.sendAccessibilityEvent(event);
+    }
+}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java
new file mode 100644
index 0000000..043266c
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java
@@ -0,0 +1,129 @@
+/*
+ * 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.Handler;
+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;
+
+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 class AccessibilityHandler extends Handler {
+        private static final int MSG_NO_HOVER_SELECTION = 0;
+
+        public AccessibilityHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+            case MSG_NO_HOVER_SELECTION:
+                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(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
new file mode 100644
index 0000000..12c59d0
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java
@@ -0,0 +1,37 @@
+/*
+ * 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
new file mode 100644
index 0000000..96f7fc9
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -0,0 +1,201 @@
+/*
+ * 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.content.SharedPreferences;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.Log;
+import android.view.MotionEvent;
+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.PointerTracker;
+
+public class AccessibleKeyboardViewProxy {
+    private static final String TAG = AccessibleKeyboardViewProxy.class.getSimpleName();
+    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 int mScaledEdgeSlop;
+    private KeyboardView mView;
+    private AccessibleKeyboardActionListener mListener;
+
+    private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
+    private int mLastX = -1;
+    private int mLastY = -1;
+
+    public static void init(Context context, SharedPreferences prefs) {
+        sInstance.initInternal(context, prefs);
+        sInstance.mListener = AccessibleInputMethodServiceProxy.getInstance();
+    }
+
+    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();
+    }
+
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event,
+            PointerTracker tracker) {
+        if (mView == null) {
+            Log.e(TAG, "No keyboard view set!");
+            return false;
+        }
+
+        switch (event.getEventType()) {
+        case AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER:
+            final Key key = tracker.getKey(mLastHoverKeyIndex);
+
+            if (key == null)
+                break;
+
+            final CharSequence description = KeyCodeDescriptionMapper.getInstance()
+                    .getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key);
+
+            if (description == null)
+                return false;
+
+            event.getText().add(description);
+
+            break;
+        }
+
+        return true;
+    }
+
+    /**
+     * Receives hover events when accessibility is turned on in API > 11. In
+     * earlier API levels, events are manually routed from onTouchEvent.
+     *
+     * @param event The hover event.
+     * @return {@code true} if the event is handled
+     */
+    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;
+    }
+
+    /**
+     * Handles touch exploration events when Accessibility is turned on.
+     *
+     * @param event The touch exploration hover event.
+     * @return {@code true} if the event was handled
+     */
+    private boolean onTouchExplorationEvent(MotionEvent event, PointerTracker tracker) {
+        final int x = (int) event.getX();
+        final int y = (int) event.getY();
+
+        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);
+            }
+
+            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());
+            }
+
+            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;
+        }
+
+        if (mView == null) {
+            Log.e(TAG, "No keyboard view set!");
+            return;
+        }
+
+        if (keyIndex == KeyDetector.NOT_A_KEY)
+            return;
+
+        final Key key = tracker.getKey(keyIndex);
+
+        if (key == null)
+            return;
+
+        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);
+        }
+    }
+
+    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);
+    }
+}
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
new file mode 100644
index 0000000..154f4af
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -0,0 +1,226 @@
+/*
+ * 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.content.SharedPreferences;
+import android.text.TextUtils;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.latin.R;
+
+import java.util.HashMap;
+
+public class KeyCodeDescriptionMapper {
+    private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
+
+    // Map of key labels to spoken description resource IDs
+    private final HashMap<CharSequence, Integer> mKeyLabelMap;
+
+    // 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 KeyCodeDescriptionMapper getInstance() {
+        return sInstance;
+    }
+
+    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) {
+        // 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);
+        mKeyCodeMap.put(Keyboard.CODE_SETTINGS, R.string.spoken_description_settings);
+        mKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_shift);
+        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);
+    }
+
+    /**
+     * 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.
+     * @param key The key from which to obtain a description.
+     * @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);
+            if (description != null)
+                return description;
+        }
+
+        if (!TextUtils.isEmpty(key.mLabel)) {
+            final String label = key.mLabel.toString().trim();
+
+            if (mKeyLabelMap.containsKey(label)) {
+                return context.getString(mKeyLabelMap.get(label));
+            } else if (label.length() == 1
+                    || (keyboard.isManualTemporaryUpperCase() && !TextUtils
+                            .isEmpty(key.mHintLetter))) {
+                return getDescriptionForKeyCode(context, keyboard, key);
+            } else {
+                return label;
+            }
+        } else if (key.mCode != Keyboard.CODE_DUMMY) {
+            return getDescriptionForKeyCode(context, keyboard, key);
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL
+     * key or {@code null} if there is not a description provided for the
+     * current keyboard context.
+     *
+     * @param context The package's context.
+     * @param keyboard The keyboard on which the key resides.
+     * @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 {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the keycode for the specified key given the current keyboard
+     * state.
+     *
+     * @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
+     */
+    private int getCorrectKeyCode(Keyboard keyboard, Key key) {
+        if (keyboard.isManualTemporaryUpperCase() && !TextUtils.isEmpty(key.mHintLetter)) {
+            return key.mHintLetter.charAt(0);
+        } else {
+            return key.mCode;
+        }
+    }
+
+    /**
+     * 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>
+     *
+     * @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.
+     * @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)) {
+            return context.getString(mKeyCodeMap.get(code));
+        } else if (Character.isDefined(code) && !Character.isISOControl(code)) {
+            return Character.toString((char) code);
+        } else {
+            return context.getString(R.string.spoken_description_unknown, code);
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java b/java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java
new file mode 100644
index 0000000..5005772
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java
@@ -0,0 +1,39 @@
+/*
+ * 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.compat;
+
+import android.view.accessibility.AccessibilityEvent;
+
+import java.lang.reflect.Field;
+
+public class AccessibilityEventCompatUtils {
+    public static final int TYPE_VIEW_HOVER_ENTER = 0x80;
+    public static final int TYPE_VIEW_HOVER_EXIT = 0x100;
+
+    private static final Field FIELD_TYPE_VIEW_HOVER_ENTER = CompatUtils.getField(
+            AccessibilityEvent.class, "TYPE_VIEW_HOVER_ENTER");
+    private static final Field FIELD_TYPE_VIEW_HOVER_EXIT = CompatUtils.getField(
+            AccessibilityEvent.class, "TYPE_VIEW_HOVER_EXIT");
+    private static final Integer OBJ_TYPE_VIEW_HOVER_ENTER = (Integer) CompatUtils
+            .getFieldValue(null, null, FIELD_TYPE_VIEW_HOVER_ENTER);
+    private static final Integer OBJ_TYPE_VIEW_HOVER_EXIT = (Integer) CompatUtils
+            .getFieldValue(null, null, FIELD_TYPE_VIEW_HOVER_EXIT);
+
+    public static boolean supportsTouchExploration() {
+        return OBJ_TYPE_VIEW_HOVER_ENTER != null && OBJ_TYPE_VIEW_HOVER_EXIT != null;
+    }
+}
diff --git a/java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java
new file mode 100644
index 0000000..4db1c7a
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java
@@ -0,0 +1,42 @@
+/*
+ * 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.compat;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.view.accessibility.AccessibilityManager;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
+public class AccessibilityManagerCompatWrapper {
+    private static final Method METHOD_getEnabledAccessibilityServiceList = CompatUtils.getMethod(
+            AccessibilityManager.class, "getEnabledAccessibilityServiceList", int.class);
+
+    private final AccessibilityManager mManager;
+
+    public AccessibilityManagerCompatWrapper(AccessibilityManager manager) {
+        mManager = manager;
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackType) {
+        return (List<AccessibilityServiceInfo>) CompatUtils.invoke(mManager,
+                Collections.<AccessibilityServiceInfo>emptyList(),
+                METHOD_getEnabledAccessibilityServiceList, feedbackType);
+    }
+}
diff --git a/java/src/com/android/inputmethod/compat/MotionEventCompatUtils.java b/java/src/com/android/inputmethod/compat/MotionEventCompatUtils.java
new file mode 100644
index 0000000..8518a4a
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/MotionEventCompatUtils.java
@@ -0,0 +1,23 @@
+/*
+ * 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.compat;
+
+public class MotionEventCompatUtils {
+    public static final int ACTION_HOVER_MOVE = 0x7;
+    public static final int ACTION_HOVER_ENTER = 0x9;
+    public static final int ACTION_HOVER_EXIT = 0xA;
+}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index 2497eeb..b91134d 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -120,13 +120,17 @@
     }
 
     public boolean isSymbolsKeyboard() {
-        return mXmlId == R.xml.kbd_symbols;
+        return mXmlId == R.xml.kbd_symbols || mXmlId == R.xml.kbd_symbols_shift;
     }
 
     public boolean isPhoneKeyboard() {
         return mMode == MODE_PHONE;
     }
 
+    public boolean isPhoneSymbolsKeyboard() {
+        return mXmlId == R.xml.kbd_phone_symbols;
+    }
+
     public boolean isNumberKeyboard() {
         return mMode == MODE_NUMBER;
     }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 7c70168..90cf3d8 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -26,6 +26,7 @@
 import android.view.View;
 import android.view.inputmethod.EditorInfo;
 
+import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
 import com.android.inputmethod.keyboard.internal.Key;
 import com.android.inputmethod.keyboard.internal.ModifierKeyState;
@@ -759,6 +760,11 @@
 
         mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
         mKeyboardView.setOnKeyboardActionListener(mInputMethodService);
+
+        // This always needs to be set since the accessibility state can
+        // potentially change without the input view being re-created.
+        AccessibleKeyboardViewProxy.setView(mKeyboardView);
+
         return mCurrentInputView;
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 4b16214..6bb8064 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -42,9 +42,12 @@
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.view.ViewGroup.MarginLayoutParams;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.PopupWindow;
 import android.widget.TextView;
 
+import com.android.inputmethod.accessibility.AccessibilityUtils;
+import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
 import com.android.inputmethod.compat.FrameLayoutCompatUtils;
 import com.android.inputmethod.keyboard.internal.Key;
 import com.android.inputmethod.keyboard.internal.MiniKeyboardBuilder;
@@ -1325,4 +1328,37 @@
     public boolean handleBack() {
         return dismissMiniKeyboard();
     }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+            return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event)
+                    || super.dispatchTouchEvent(event);
+        }
+
+        return super.dispatchTouchEvent(event);
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+            final PointerTracker tracker = getPointerTracker(0);
+            return AccessibleKeyboardViewProxy.getInstance().dispatchPopulateAccessibilityEvent(
+                    event, tracker) || super.dispatchPopulateAccessibilityEvent(event);
+        }
+
+        return super.dispatchPopulateAccessibilityEvent(event);
+    }
+
+    public boolean onHoverEvent(MotionEvent event) {
+        // Since reflection doesn't support calling superclass methods, this
+        // method checks for the existence of onHoverEvent() in the View class
+        // before returning a value.
+        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+            final PointerTracker tracker = getPointerTracker(0);
+            return AccessibleKeyboardViewProxy.getInstance().onHoverEvent(event, tracker);
+        }
+
+        return false;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 1645b16..9c6465d 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -53,6 +53,7 @@
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.InputConnection;
 
+import com.android.inputmethod.accessibility.AccessibilityUtils;
 import com.android.inputmethod.compat.CompatUtils;
 import com.android.inputmethod.compat.EditorInfoCompatUtils;
 import com.android.inputmethod.compat.InputConnectionCompatUtils;
@@ -353,6 +354,7 @@
         SubtypeSwitcher.init(this, prefs);
         KeyboardSwitcher.init(this, prefs);
         Recorrection.init(this, prefs);
+        AccessibilityUtils.init(this, prefs);
 
         super.onCreate();