Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9a81ce92 authored by alanv's avatar alanv
Browse files

Added virtual view hierarchy for keyboard accessibility.

Bug: 5829051
Change-Id: Ied1b6267eec616bd3b9337f6e761b0c740aa0eb2
parent 5a0661ea
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ LOCAL_REQUIRED_MODULES := libjni_latinime

LOCAL_STATIC_JAVA_LIBRARIES := android-common
LOCAL_STATIC_JAVA_LIBRARIES += inputmethod-common
LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4

# Do not compress dictionary files to mmap dict data runtime
LOCAL_AAPT_FLAGS := -0 .dict
+377 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.inputmethod.accessibility;

import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
import android.support.v4.view.ViewCompat;
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.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo;

import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardView;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * Exposes a virtual view sub-tree for {@link KeyboardView} and generates
 * {@link AccessibilityEvent}s for individual {@link Key}s.
 * <p>
 * A virtual sub-tree is composed of imaginary {@link View}s that are reported
 * as a part of the view hierarchy for accessibility purposes. This enables
 * custom views that draw complex content to report them selves as a tree of
 * virtual views, thus conveying their logical structure.
 * </p>
 */
public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat {
    private static final String TAG = AccessibilityEntityProvider.class.getSimpleName();

    private final KeyboardView mKeyboardView;
    private final InputMethodService mInputMethodService;
    private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
    private final AccessibilityUtils mAccessibilityUtils;

    /** A map of integer IDs to {@link Key}s. */
    private final SparseArray<Key> mVirtualViewIdToKey = new SparseArray<Key>();

    /** Temporary rect used to calculate in-screen bounds. */
    private final Rect mTempBoundsInScreen = new Rect();

    /** The parent view's cached on-screen location. */
    private final int[] mParentLocation = new int[2];

    public AccessibilityEntityProvider(KeyboardView keyboardView, InputMethodService inputMethod) {
        mKeyboardView = keyboardView;
        mInputMethodService = inputMethod;

        mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
        mAccessibilityUtils = AccessibilityUtils.getInstance();

        assignVirtualViewIds();
        updateParentLocation();

        // Ensure that the on-screen bounds are cleared when the layout changes.
        mKeyboardView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
    }

    /**
     * Creates and populates an {@link AccessibilityEvent} for the specified key
     * and event type.
     *
     * @param key A key on the host keyboard view.
     * @param eventType The event type to create.
     * @return A populated {@link AccessibilityEvent} for the key.
     * @see AccessibilityEvent
     */
    public AccessibilityEvent createAccessibilityEvent(Key key, int eventType) {
        final int virtualViewId = generateVirtualViewIdForKey(key);
        final String keyDescription = getKeyDescription(key);

        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
        event.setPackageName(mKeyboardView.getContext().getPackageName());
        event.setClassName(key.getClass().getName());
        event.getText().add(keyDescription);

        final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
        record.setSource(mKeyboardView, virtualViewId);

        return event;
    }

    /**
     * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
     * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
     * the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
     * <p>
     * A virtual descendant is an imaginary View that is reported as a part of
     * the view hierarchy for accessibility purposes. This enables custom views
     * that draw complex content to report them selves as a tree of virtual
     * views, thus conveying their logical structure.
     * </p>
     * <p>
     * The implementer is responsible for obtaining an accessibility node info
     * from the pool of reusable instances and setting the desired properties of
     * the node info before returning it.
     * </p>
     *
     * @param virtualViewId A client defined virtual view id.
     * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual
     *         descendant or the host View.
     * @see AccessibilityNodeInfoCompat
     */
    @Override
    public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
        AccessibilityNodeInfoCompat info = null;

        if (virtualViewId == View.NO_ID) {
            // We are requested to create an AccessibilityNodeInfo describing
            // this View, i.e. the root of the virtual sub-tree.
            info = AccessibilityNodeInfoCompat.obtain(mKeyboardView);
            ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, info);

            // Add the virtual children of the root View.
            // TODO(alanv): Need to assign a unique ID to each key.
            final Keyboard keyboard = mKeyboardView.getKeyboard();
            final Set<Key> keys = keyboard.mKeys;
            for (Key key : keys) {
                final int childVirtualViewId = generateVirtualViewIdForKey(key);
                info.addChild(mKeyboardView, childVirtualViewId);
            }
        } else {
            // Find the view that corresponds to the given id.
            final Key key = mVirtualViewIdToKey.get(virtualViewId);
            if (key == null) {
                Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
                return null;
            }

            final String keyDescription = getKeyDescription(key);
            final Rect boundsInParent = key.mHitBox;

            // Calculate the key's in-screen bounds.
            mTempBoundsInScreen.set(boundsInParent);
            mTempBoundsInScreen.offset(mParentLocation[0], mParentLocation[1]);

            final Rect boundsInScreen = mTempBoundsInScreen;

            // Obtain and initialize an AccessibilityNodeInfo with
            // information about the virtual view.
            info = AccessibilityNodeInfoCompat.obtain();
            info.addAction(AccessibilityNodeInfoCompat.ACTION_SELECT);
            info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION);
            info.setPackageName(mKeyboardView.getContext().getPackageName());
            info.setClassName(key.getClass().getName());
            info.setBoundsInParent(boundsInParent);
            info.setBoundsInScreen(boundsInScreen);
            info.setParent(mKeyboardView);
            info.setSource(mKeyboardView, virtualViewId);
            info.setBoundsInScreen(boundsInScreen);
            info.setText(keyDescription);
        }

        return info;
    }

    /**
     * Performs an accessibility action on a virtual view, i.e. a descendant of
     * the host View, with the given <code>virtualViewId</code> or the host View itself if
     * <code>virtualViewId</code> equals to {@link View#NO_ID}.
     *
     * @param action The action to perform.
     * @param virtualViewId A client defined virtual view id.
     * @return True if the action was performed.
     * @see #createAccessibilityNodeInfo(int)
     * @see AccessibilityNodeInfoCompat
     */
    @Override
    public boolean performAccessibilityAction(int action, int virtualViewId) {
        if (virtualViewId == View.NO_ID) {
            // Perform the action on the host View.
            switch (action) {
            case AccessibilityNodeInfoCompat.ACTION_SELECT:
                if (!mKeyboardView.isSelected()) {
                    mKeyboardView.setSelected(true);
                    return mKeyboardView.isSelected();
                }
                break;
            case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION:
                if (mKeyboardView.isSelected()) {
                    mKeyboardView.setSelected(false);
                    return !mKeyboardView.isSelected();
                }
                break;
            }
        } else {
            // Find the view that corresponds to the given id.
            final Key child = mVirtualViewIdToKey.get(virtualViewId);
            if (child == null)
                return false;

            // Perform the action on a virtual view.
            switch (action) {
            case AccessibilityNodeInfoCompat.ACTION_SELECT:
                // TODO: Provide some focus indicator.
                return true;
            case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION:
                // TODO: Provide some clear focus indicator.
                return true;
            }
        }

        return false;
    }

    /**
     * Finds {@link AccessibilityNodeInfoCompat}s by text. The match is case
     * insensitive containment. The search is relative to the virtual view, i.e.
     * a descendant of the host View, with the given <code>virtualViewId</code> or the host
     * View itself <code>virtualViewId</code> equals to {@link View#NO_ID}.
     *
     * @param virtualViewId A client defined virtual view id which defined the
     *            root of the tree in which to perform the search.
     * @param text The searched text.
     * @return A list of node info.
     * @see #createAccessibilityNodeInfo(int)
     * @see AccessibilityNodeInfoCompat
     */
    @Override
    public List<AccessibilityNodeInfoCompat> findAccessibilityNodeInfosByText(
            String text, int virtualViewId) {
        final String searchedLowerCase = text.toLowerCase();
        final Keyboard keyboard = mKeyboardView.getKeyboard();

        List<AccessibilityNodeInfoCompat> results = null;

        if (virtualViewId == View.NO_ID) {
            for (Key key : keyboard.mKeys) {
                results = findByTextAndPopulate(searchedLowerCase, key, results);
            }
        } else {
            final Key key = mVirtualViewIdToKey.get(virtualViewId);

            results = findByTextAndPopulate(searchedLowerCase, key, results);
        }

        if (results == null) {
            return Collections.emptyList();
        }

        return results;
    }

    /**
     * Helper method for {@link #findAccessibilityNodeInfosByText(String, int)}.
     * Takes a current set of results and matches a specified key against a
     * lower-case search string. Returns an updated list of results.
     *
     * @param searchedLowerCase The lower-case search string.
     * @param key The key to compare against.
     * @param results The current list of results, or {@code null} if no results
     *            found.
     * @return An updated list of results, or {@code null} if no results found.
     */
    private List<AccessibilityNodeInfoCompat> findByTextAndPopulate(String searchedLowerCase,
            Key key, List<AccessibilityNodeInfoCompat> results) {
        if (!keyContainsText(key, searchedLowerCase)) {
            return results;
        }

        final int childVirtualViewId = generateVirtualViewIdForKey(key);
        final AccessibilityNodeInfoCompat nodeInfo = createAccessibilityNodeInfo(
                childVirtualViewId);

        if (results == null) {
            results = new LinkedList<AccessibilityNodeInfoCompat>();
        }

        results.add(nodeInfo);

        return results;
    }

    /**
     * Returns whether a key's current description contains the lower-case
     * search text.
     *
     * @param key The key to compare against.
     * @param textLowerCase The lower-case search string.
     * @return {@code true} if the key contains the search text.
     */
    private boolean keyContainsText(Key key, String textLowerCase) {
        if (key == null) {
            return false;
        }

        final String description = getKeyDescription(key);

        if (description == null) {
            return false;
        }

        return description.toLowerCase().contains(textLowerCase);
    }

    /**
     * Returns the context-specific description for a {@link Key}.
     *
     * @param key The key to describe.
     * @return The context-specific description of the key.
     */
    private String getKeyDescription(Key key) {
        final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
        final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
        final String keyDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
                mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure);

        return keyDescription;
    }

    /**
     * Assigns virtual view IDs to keyboard keys and populates the related maps.
     */
    private void assignVirtualViewIds() {
        final Keyboard keyboard = mKeyboardView.getKeyboard();
        if (keyboard == null) {
            return;
        }

        mVirtualViewIdToKey.clear();

        final Set<Key> keySet = keyboard.mKeys;
        for (Key key : keySet) {
            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 specified key.
     *
     * @param key The key to identify.
     * @return A virtual view identifier.
     */
    private static int generateVirtualViewIdForKey(Key key) {
        // The key code is unique within an instance of a Keyboard.
        return key.mCode;
    }

    private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            assignVirtualViewIds();
            updateParentLocation();
        }
    };
}
+5 −6
Original line number Diff line number Diff line
@@ -21,13 +21,14 @@ import android.inputmethodservice.InputMethodService;
import android.media.AudioManager;
import android.os.SystemClock;
import android.provider.Settings;
import android.support.v4.view.MotionEventCompat;
import android.util.Log;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.EditorInfo;

import com.android.inputmethod.compat.AccessibilityManagerCompatWrapper;
import com.android.inputmethod.compat.AccessibilityManagerCompatUtils;
import com.android.inputmethod.compat.AudioManagerCompatWrapper;
import com.android.inputmethod.compat.InputTypeCompatUtils;
import com.android.inputmethod.compat.MotionEventCompatUtils;
@@ -44,7 +45,6 @@ public class AccessibilityUtils {

    private Context mContext;
    private AccessibilityManager mAccessibilityManager;
    private AccessibilityManagerCompatWrapper mCompatManager;
    private AudioManagerCompatWrapper mAudioManager;

    /*
@@ -77,7 +77,6 @@ public class AccessibilityUtils {
        mContext = context;
        mAccessibilityManager = (AccessibilityManager) context
                .getSystemService(Context.ACCESSIBILITY_SERVICE);
        mCompatManager = new AccessibilityManagerCompatWrapper(mAccessibilityManager);

        final AudioManager audioManager = (AudioManager) context
                .getSystemService(Context.AUDIO_SERVICE);
@@ -94,7 +93,7 @@ public class AccessibilityUtils {
    public boolean isTouchExplorationEnabled() {
        return ENABLE_ACCESSIBILITY
                && mAccessibilityManager.isEnabled()
                && mCompatManager.isTouchExplorationEnabled();
                && AccessibilityManagerCompatUtils.isTouchExplorationEnabled(mAccessibilityManager);
    }

    /**
@@ -110,7 +109,7 @@ public class AccessibilityUtils {

        return action == MotionEventCompatUtils.ACTION_HOVER_ENTER
                || action == MotionEventCompatUtils.ACTION_HOVER_EXIT
                || action == MotionEventCompatUtils.ACTION_HOVER_MOVE;
                || action == MotionEventCompat.ACTION_HOVER_MOVE;
    }

    /**
+108 −56

File changed.

Preview size limit exceeded, changes collapsed.

+3 −2
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.inputmethod.accessibility;

import android.content.Context;
import android.os.Message;
import android.support.v4.view.MotionEventCompat;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

@@ -32,7 +33,7 @@ import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
 * properties:
 * <ul>
 *   <li>Begins with a {@link MotionEventCompatUtils#ACTION_HOVER_ENTER} event
 *   <li>Contains any number of {@link MotionEventCompatUtils#ACTION_HOVER_MOVE}
 *   <li>Contains any number of {@link MotionEventCompat#ACTION_HOVER_MOVE}
 *       events
 *   <li>Ends with a {@link MotionEventCompatUtils#ACTION_HOVER_EXIT} event
 *   <li>Maximum duration of 250 milliseconds
@@ -128,7 +129,7 @@ public abstract class FlickGestureDetector {
        final float distanceSquare = calculateDistanceSquare(mCachedHoverEnter, event);

        switch (event.getAction()) {
        case MotionEventCompatUtils.ACTION_HOVER_MOVE:
        case MotionEventCompat.ACTION_HOVER_MOVE:
            // Consume all valid move events before timeout.
            return true;
        case MotionEventCompatUtils.ACTION_HOVER_EXIT:
Loading