Make LatinIME keys accessibility focusable, clickable.

Also fix speech for labeled keys.

Bug: 6498563
Change-Id: I094d4db0e57fa373759a63eb3354b1ab3ab0f525
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
index 955cb4c..67e21b1 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
@@ -18,13 +18,18 @@
 
 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.ViewParent;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.inputmethod.EditorInfo;
@@ -45,6 +50,7 @@
  */
 public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat {
     private static final String TAG = AccessibilityEntityProvider.class.getSimpleName();
+    private static final int UNDEFINED = Integer.MIN_VALUE;
 
     private final KeyboardView mKeyboardView;
     private final InputMethodService mInputMethodService;
@@ -60,6 +66,9 @@
     /** The parent view's cached on-screen location. */
     private final int[] mParentLocation = new int[2];
 
+    /** The virtual view identifier for the focused node. */
+    private int mAccessibilityFocusedView = UNDEFINED;
+
     public AccessibilityEntityProvider(KeyboardView keyboardView, InputMethodService inputMethod) {
         mKeyboardView = keyboardView;
         mInputMethodService = inputMethod;
@@ -124,7 +133,9 @@
     public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
         AccessibilityNodeInfoCompat info = null;
 
-        if (virtualViewId == View.NO_ID) {
+        if (virtualViewId == UNDEFINED) {
+            return null;
+        } else  if (virtualViewId == View.NO_ID) {
             // We are requested to create an AccessibilityNodeInfo describing
             // this View, i.e. the root of the virtual sub-tree.
             info = AccessibilityNodeInfoCompat.obtain(mKeyboardView);
@@ -166,12 +177,115 @@
             info.setSource(mKeyboardView, virtualViewId);
             info.setBoundsInScreen(boundsInScreen);
             info.setEnabled(true);
+            info.setClickable(true);
+            info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
+
+            if (mAccessibilityFocusedView == virtualViewId) {
+                info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+            } else {
+                info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
+            }
         }
 
         return info;
     }
 
     /**
+     * Simulates a key press by injecting touch events into the keyboard view.
+     * This avoids the complexity of trackers and listeners within the keyboard.
+     *
+     * @param key The key to press.
+     */
+    void simulateKeyPress(Key key) {
+        final int x = key.mX + (key.mWidth / 2);
+        final int y = key.mY + (key.mHeight / 2);
+        final long downTime = SystemClock.uptimeMillis();
+        final MotionEvent downEvent = MotionEvent.obtain(
+                downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0);
+        final MotionEvent upEvent = MotionEvent.obtain(
+                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0);
+
+        mKeyboardView.onTouchEvent(downEvent);
+        mKeyboardView.onTouchEvent(upEvent);
+    }
+
+    @Override
+    public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+        final Key key = mVirtualViewIdToKey.get(virtualViewId);
+
+        if (key == null) {
+            return false;
+        }
+
+        return performActionForKey(key, action, arguments);
+    }
+
+    /**
+     * Performs the specified accessibility action for the given key.
+     *
+     * @param key The on which to perform the action.
+     * @param action The action to perform.
+     * @param arguments The action's arguments.
+     * @return The result of performing the action, or false if the action is
+     *         not supported.
+     */
+    boolean performActionForKey(Key key, int action, Bundle arguments) {
+        final int virtualViewId = generateVirtualViewIdForKey(key);
+
+        switch (action) {
+        case AccessibilityNodeInfoCompat.ACTION_CLICK:
+            simulateKeyPress(key);
+            return true;
+        case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
+            if (mAccessibilityFocusedView == virtualViewId) {
+                return false;
+            }
+            mAccessibilityFocusedView = virtualViewId;
+            sendAccessibilityEventForKey(
+                    key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+            return true;
+        case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+            if (mAccessibilityFocusedView != virtualViewId) {
+                return false;
+            }
+            mAccessibilityFocusedView = UNDEFINED;
+            sendAccessibilityEventForKey(
+                    key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public AccessibilityNodeInfoCompat findAccessibilityFocus(int virtualViewId) {
+        return createAccessibilityNodeInfo(mAccessibilityFocusedView);
+    }
+
+    @Override
+    public AccessibilityNodeInfoCompat accessibilityFocusSearch(int direction, int virtualViewId) {
+        // Focus search is not currently supported for IMEs.
+        return null;
+    }
+
+    /**
+     * Sends an accessibility event for the given {@link Key}.
+     *
+     * @param key The key that's sending the event.
+     * @param eventType The type of event to send.
+     */
+    void sendAccessibilityEventForKey(Key key, int eventType) {
+        final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
+        final ViewParent parent = mKeyboardView.getParent();
+
+        if (parent == null) {
+            return;
+        }
+
+        parent.requestSendAccessibilityEvent(mKeyboardView, event);
+    }
+
+    /**
      * Returns the context-specific description for a {@link Key}.
      *
      * @param key The key to describe.
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
index 34817ba..1b0e488 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -21,10 +21,10 @@
 import android.support.v4.view.AccessibilityDelegateCompat;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.view.accessibility.AccessibilityEvent;
 
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
@@ -91,13 +91,7 @@
      */
     @Override
     public AccessibilityEntityProvider getAccessibilityNodeProvider(View host) {
-        // 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;
+        return getAccessibilityNodeProvider();
     }
 
     /**
@@ -120,7 +114,7 @@
             // Make sure we're not getting an EXIT event because the user slid
             // off the keyboard area, then force a key press.
             if (pointInView(x, y)) {
-                tracker.onRegisterKey(key);
+                getAccessibilityNodeProvider().simulateKeyPress(key);
             }
             //$FALL-THROUGH$
         case MotionEvent.ACTION_HOVER_ENTER:
@@ -137,6 +131,19 @@
     }
 
     /**
+     * @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.
@@ -191,12 +198,18 @@
             return false;
         }
 
+        final AccessibilityEntityProvider provider = getAccessibilityNodeProvider();
+
         switch (event.getAction()) {
         case MotionEvent.ACTION_HOVER_ENTER:
-            sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
+            provider.sendAccessibilityEventForKey(
+                    key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
+            provider.performActionForKey(
+                    key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
             break;
         case MotionEvent.ACTION_HOVER_EXIT:
-            sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
+            provider.sendAccessibilityEventForKey(
+                    key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
             break;
         }
 
@@ -204,20 +217,6 @@
     }
 
     /**
-     * Populates and sends an {@link AccessibilityEvent} for the specified key.
-     *
-     * @param key The key to send an event for.
-     * @param eventType The type of event to send.
-     */
-    private void sendAccessibilityEventForKey(Key key, int eventType) {
-        final AccessibilityEntityProvider nodeProvider = getAccessibilityNodeProvider(null);
-        final AccessibilityEvent event = nodeProvider.createAccessibilityEvent(key, eventType);
-
-        // Propagates the event up the view hierarchy.
-        mView.getParent().requestSendAccessibilityEvent(mView, event);
-    }
-
-    /**
      * Notifies the user of changes in the keyboard shift state.
      */
     public void notifyShiftState() {
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index 3d861c2..f4e4105 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -111,6 +111,9 @@
             if (mKeyLabelMap.containsKey(label)) {
                 return context.getString(mKeyLabelMap.get(label));
             }
+
+            // Otherwise, return the label.
+            return key.mLabel;
         }
 
         // Just attempt to speak the description.