com.android.inputmethod.accessibility.KeyboardAccessibilityNodeProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.android.inputmethod.accessibility.KeyboardAccessibilityNodeProvider.java

Source

/*
 * 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.os.Bundle;
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.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo;

import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardView;
import com.android.inputmethod.latin.common.CoordinateUtils;
import com.android.inputmethod.latin.settings.Settings;
import com.android.inputmethod.latin.settings.SettingsValues;

import java.util.List;

/**
 * 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>
 */
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 KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
    private final AccessibilityUtils mAccessibilityUtils;

    /** 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 = CoordinateUtils.newInstance();

    /** The virtual view identifier for the focused node. */
    private int mAccessibilityFocusedView = UNDEFINED;

    /** 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;

    /** The current keyboard. */
    private Keyboard mKeyboard;

    public KeyboardAccessibilityNodeProvider(final KV keyboardView,
            final KeyboardAccessibilityDelegate<KV> delegate) {
        super();
        mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
        mAccessibilityUtils = AccessibilityUtils.getInstance();
        mKeyboardView = keyboardView;
        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(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(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;
    }

    /**
     * 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(final Key key, final int eventType) {
        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 = 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
     * 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(final int virtualViewId) {
        if (virtualViewId == UNDEFINED) {
            return 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.
            final AccessibilityNodeInfoCompat rootInfo = AccessibilityNodeInfoCompat.obtain(mKeyboardView);
            ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo);
            updateParentLocation();

            // Add the virtual children of the root View.
            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 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;
        }
        final String keyDescription = getKeyDescription(key);
        final Rect boundsInParent = key.getHitBox();

        // Calculate the key's in-screen bounds.
        mTempBoundsInScreen.set(boundsInParent);
        mTempBoundsInScreen.offset(CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation));
        final Rect boundsInScreen = mTempBoundsInScreen;

        // Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
        info.setPackageName(mKeyboardView.getContext().getPackageName());
        info.setClassName(key.getClass().getName());
        info.setContentDescription(keyDescription);
        info.setBoundsInParent(boundsInParent);
        info.setBoundsInScreen(boundsInScreen);
        info.setParent(mKeyboardView);
        info.setSource(mKeyboardView, virtualViewId);
        info.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);
        } else {
            info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
        }
        return info;
    }

    @Override
    public boolean performAction(final int virtualViewId, final int action, final Bundle arguments) {
        final Key key = getKeyOf(virtualViewId);
        if (key == null) {
            return false;
        }
        return performActionForKey(key, action);
    }

    /**
     * Performs the specified accessibility action for the given key.
     *
     * @param key The on which to perform the action.
     * @param action The action to perform.
     * @return The result of performing the action, or false if the action is not supported.
     */
    boolean performActionForKey(final Key key, final int action) {
        switch (action) {
        case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
            mAccessibilityFocusedView = getVirtualViewIdOf(key);
            sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
            return true;
        case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
            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;
        }
    }

    /**
     * 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(final Key key, final int eventType) {
        final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
        mAccessibilityUtils.requestSendAccessibilityEvent(event);
    }

    /**
     * Returns the context-specific description for a {@link Key}.
     *
     * @param key The key to describe.
     * @return The context-specific description of the key.
     */
    private String getKeyDescription(final Key key) {
        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(),
                mKeyboard, key, shouldObscure);
        if (currentSettings.isWordSeparator(key.getCode())) {
            return mAccessibilityUtils.getAutoCorrectionDescription(keyCodeDescription, shouldObscure);
        }
        return keyCodeDescription;
    }

    /**
     * Updates the parent's on-screen location.
     */
    private void updateParentLocation() {
        mKeyboardView.getLocationOnScreen(mParentLocation);
    }
}