com.android.talkback.eventprocessor.ProcessorScrollPosition.java Source code

Java tutorial

Introduction

Here is the source code for com.android.talkback.eventprocessor.ProcessorScrollPosition.java

Source

/*
 * 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.talkback.eventprocessor;

import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityRecord;
import com.android.talkback.R;
import com.android.talkback.SpeechController;
import com.android.talkback.controller.CursorController;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.Role;
import com.android.utils.StringBuilderUtils;
import com.google.android.marvin.talkback.TalkBackService;
import com.android.talkback.controller.FullScreenReadController;
import com.android.utils.AccessibilityEventListener;
import com.android.utils.WeakReferenceHandler;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;

/**
 * Manages scroll position feedback. If a VIEW_SCROLLED event passes through
 * this processor and no further events are received for a specified duration, a
 * "scroll position" message is spoken.
 */
public class ProcessorScrollPosition implements AccessibilityEventListener, CursorController.ScrollListener {
    /** Default pitch adjustment for text event feedback. */
    private static final float DEFAULT_PITCH = 1.2f;

    /** Default rate adjustment for text event feedback. */
    private static final float DEFAULT_RATE = 1.0f;

    private final HashMap<EventId, Integer> mCachedFromValues = new HashMap<>();
    private final HashMap<EventId, Integer> mCachedItemCounts = new HashMap<>();
    private final Bundle mSpeechParams = new Bundle();
    private final ScrollPositionHandler mHandler = new ScrollPositionHandler(this);

    private final Context mContext;
    private final SpeechController mSpeechController;
    private final FullScreenReadController mFullScreenReadController;

    /** The last node that was auto-scrolled by the CursorController. */
    private AccessibilityNodeInfoCompat mAutoScrollNode;

    public ProcessorScrollPosition(FullScreenReadController fullScreenReadController,
            SpeechController speechController, CursorController cursorController, TalkBackService context) {
        if (speechController == null)
            throw new IllegalStateException();
        if (fullScreenReadController == null)
            throw new IllegalStateException();
        if (cursorController == null)
            throw new IllegalStateException();
        mContext = context;
        mSpeechController = speechController;
        mFullScreenReadController = fullScreenReadController;
        mSpeechParams.putFloat(SpeechController.SpeechParam.PITCH, DEFAULT_PITCH);
        mSpeechParams.putFloat(SpeechController.SpeechParam.RATE, DEFAULT_RATE);
        cursorController.addScrollListener(this);
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (shouldIgnoreEvent(event)) {
            return;
        }

        switch (event.getEventType()) {
        case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
            // Window state changes clear the cache.
            mCachedFromValues.clear();
            mCachedItemCounts.clear();
            mHandler.cancelScrollFeedback();
            break;
        case AccessibilityEvent.TYPE_VIEW_SCROLLED:
        case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
            mHandler.postScrollFeedback(event);
            break;
        }
    }

    @Override
    public void onScroll(AccessibilityNodeInfoCompat scrolledNode, int action, boolean auto) {
        AccessibilityNodeInfoUtils.recycleNodes(mAutoScrollNode);
        if (auto) {
            mAutoScrollNode = AccessibilityNodeInfoCompat.obtain(scrolledNode);
        } else {
            mAutoScrollNode = null;
        }
    }

    private boolean shouldIgnoreEvent(AccessibilityEvent event) {
        switch (event.getEventType()) {
        case AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
        case AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
            return true;
        case AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED:
        case AccessibilityEventCompat.TYPE_VIEW_SCROLLED:
            return shouldIgnoreUpdateListEvent(event);
        default:
            return false;
        }
    }

    private boolean shouldIgnoreUpdateListEvent(AccessibilityEvent event) {
        // Don't speak during full-screen read.
        if (mFullScreenReadController.isActive()) {
            return true;
        }

        final int fromIndex = event.getFromIndex() + 1;
        final int itemCount = event.getItemCount();
        if (itemCount <= 0 || fromIndex <= 0) {
            return true;
        }

        EventId eventId;
        try {
            eventId = new EventId(event);
        } catch (Exception e) {
            return true;
        }

        final Integer cachedFromIndex = mCachedFromValues.get(eventId);
        final Integer cachedItemCount = mCachedItemCounts.get(eventId);

        if ((cachedFromIndex != null) && (cachedFromIndex == fromIndex) && (cachedItemCount != null)
                && (cachedItemCount == itemCount)) {
            // The from index hasn't changed, which means the event is coming
            // from a re-layout or resize and should not be spoken.
            return true;
        }

        // The behavior of put() for an existing key is unspecified, so we can't
        // recycle the old or new key nodes.
        mCachedFromValues.put(eventId, fromIndex);
        mCachedItemCounts.put(eventId, itemCount);

        // Allow the list indices to be cached, but don't actually speak after auto-scroll.
        if (mAutoScrollNode != null) {
            AccessibilityNodeInfo source = event.getSource();
            if (source != null) {
                try {
                    if (source.equals(mAutoScrollNode.getInfo())) {
                        mAutoScrollNode.recycle();
                        mAutoScrollNode = null;
                        return true;
                    }
                } finally {
                    source.recycle();
                }
            }
        }

        return false;
    }

    /**
     * Given an {@link AccessibilityEvent}, speaks a scroll position.
     *
     * @param event The source event.
     */
    private void handleScrollFeedback(AccessibilityEvent event) {
        final CharSequence text;
        AccessibilityNodeInfo source = event.getSource();
        if (Role.getRole(source) == Role.ROLE_PAGER) {
            text = getDescriptionForPageEvent(event, source);
        } else {
            text = getDescriptionForScrollEvent(event);
        }
        if (source != null) {
            source.recycle();
        }

        if (TextUtils.isEmpty(text)) {
            return;
        }

        // don't pronounce non-visible nodes
        AccessibilityNodeInfo node = event.getSource();
        if (node != null && !node.isVisibleToUser()) {
            return;
        }

        // Use QUEUE mode so that we don't interrupt more important messages.
        mSpeechController.speak(text, SpeechController.QUEUE_MODE_QUEUE, 0, mSpeechParams);
    }

    private CharSequence getDescriptionForScrollEvent(AccessibilityEvent event) {
        // If the from index or item count are invalid, don't announce anything.
        final int fromIndex = (event.getFromIndex() + 1);
        final int itemCount = event.getItemCount();
        if ((fromIndex <= 0) || (itemCount <= 0)) {
            return null;
        }

        // If the to and from indices are the same, or if the to index is
        // invalid, only announce the item at the from index.
        final int toIndex = event.getToIndex() + 1;
        if ((fromIndex == toIndex) || (toIndex <= 0) || (toIndex > itemCount)) {
            return mContext.getString(R.string.template_scroll_from_count, fromIndex, itemCount);
        }

        // Announce the range of visible items.
        return mContext.getString(R.string.template_scroll_from_to_count, fromIndex, toIndex, itemCount);
    }

    private CharSequence getDescriptionForPageEvent(AccessibilityEvent event, AccessibilityNodeInfo source) {
        final int fromIndex = (event.getFromIndex() + 1);
        final int itemCount = event.getItemCount();
        if ((fromIndex <= 0) || (itemCount <= 0)) {
            return null;
        }

        CharSequence pageTitle = getSelectedPageTitle(source);
        if (!TextUtils.isEmpty(pageTitle)) {
            CharSequence count = mContext.getString(R.string.template_viewpager_index_count_short, fromIndex,
                    itemCount);

            SpannableStringBuilder output = new SpannableStringBuilder();
            StringBuilderUtils.appendWithSeparator(output, pageTitle, count);
            return output;
        }

        return mContext.getString(R.string.template_viewpager_index_count, fromIndex, itemCount);
    }

    private static CharSequence getSelectedPageTitle(AccessibilityNodeInfo node) {
        // We need to refresh() after the scroll to get an accurate page title but we can only
        // do that on API 18+.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            return null;
        }

        if (node == null) {
            return null;
        }

        AccessibilityNodeInfoCompat nodeCompat = new AccessibilityNodeInfoCompat(node);
        nodeCompat.refresh();

        int numChildren = nodeCompat.getChildCount(); // Not the number of pages!
        CharSequence title = null;
        for (int i = 0; i < numChildren; ++i) {
            AccessibilityNodeInfoCompat child = nodeCompat.getChild(i);
            if (child != null) {
                try {
                    if (child.isVisibleToUser()) {
                        if (title == null) {
                            // Try to roughly match RulePagerPage, which uses getNodeText
                            // (but completely matching all the time is not critical).
                            title = AccessibilityNodeInfoUtils.getNodeText(child);
                        } else {
                            // Multiple visible children, abort.
                            return null;
                        }
                    }
                } finally {
                    child.recycle();
                }
            }
        }

        return title;
    }

    private static class ScrollPositionHandler extends WeakReferenceHandler<ProcessorScrollPosition> {
        /** Message identifier for a scroll position notification. */
        private static final int SCROLL_FEEDBACK = 1;

        /** Delay before reading a scroll position notification. */
        private static final long DELAY_SCROLL_FEEDBACK = 1000;

        /** Delay before reading a page position notification. */
        private static final long DELAY_PAGE_FEEDBACK = 500;

        public ScrollPositionHandler(ProcessorScrollPosition parent) {
            super(parent);
        }

        @Override
        public void handleMessage(Message msg, ProcessorScrollPosition parent) {
            final AccessibilityEvent event = (AccessibilityEvent) msg.obj;
            switch (msg.what) {
            case SCROLL_FEEDBACK:
                parent.handleScrollFeedback(event);
                break;
            }

            event.recycle();
        }

        /**
         * Posts the delayed scroll position feedback. Call this for every
         * VIEW_SCROLLED event.
         */
        private void postScrollFeedback(AccessibilityEvent event) {
            cancelScrollFeedback();
            final AccessibilityEvent eventClone = AccessibilityEvent.obtain(event);
            final Message msg = obtainMessage(SCROLL_FEEDBACK, eventClone);

            AccessibilityNodeInfo source = event.getSource();
            if (Role.getRole(source) == Role.ROLE_PAGER) {
                sendMessageDelayed(msg, DELAY_PAGE_FEEDBACK);
            } else {
                sendMessageDelayed(msg, DELAY_SCROLL_FEEDBACK);
            }
            if (source != null) {
                source.recycle();
            }
        }

        /**
         * Removes any pending scroll position feedback. Call this for every
         * event.
         */
        private void cancelScrollFeedback() {
            removeMessages(SCROLL_FEEDBACK);
        }
    }

    private static class EventId {
        public long nodeId;
        public int windowId;
        private final int hashcode;

        private static Method sGetSourceNodeIdMethod;
        private static final String LOGTAG = "EventId";
        static {
            try {
                sGetSourceNodeIdMethod = AccessibilityRecord.class.getDeclaredMethod("getSourceNodeId");
                sGetSourceNodeIdMethod.setAccessible(true);
            } catch (NoSuchMethodException e) {
                Log.d(LOGTAG, "Error setting up fields: " + e.toString());
                e.printStackTrace();
            }
        }

        public EventId(long nodeId, int windowId) {
            this.nodeId = nodeId;
            this.windowId = windowId;
            hashcode = (int) (nodeId ^ (nodeId >>> 32)) + windowId * 7;
        }

        public EventId(AccessibilityEvent event) throws InvocationTargetException, IllegalAccessException {
            this((long) sGetSourceNodeIdMethod.invoke(event), event.getWindowId());
        }

        @Override
        public boolean equals(Object other) {
            if (!(other instanceof EventId)) {
                return false;
            }

            EventId otherId = (EventId) other;
            return windowId == otherId.windowId && nodeId == otherId.nodeId;
        }

        @Override
        public int hashCode() {
            return hashcode;
        }
    }
}