| /* |
| * Copyright (C) 2012 Google Inc. |
| * |
| * 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.googlecode.eyesfree.utils; |
| |
| import android.content.Context; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.support.v4.view.AccessibilityDelegateCompat; |
| 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.text.TextUtils; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityManager; |
| |
| import java.util.LinkedList; |
| import java.util.List; |
| |
| public abstract class TouchExplorationHelper<T> extends AccessibilityNodeProviderCompat |
| implements View.OnHoverListener { |
| /** Virtual node identifier value for invalid nodes. */ |
| public static final int INVALID_ID = Integer.MIN_VALUE; |
| |
| private final Rect mTempScreenRect = new Rect(); |
| private final Rect mTempParentRect = new Rect(); |
| private final Rect mTempVisibleRect = new Rect(); |
| private final int[] mTempGlobalRect = new int[2]; |
| |
| private final AccessibilityManager mManager; |
| |
| private View mParentView; |
| private int mFocusedItemId = INVALID_ID; |
| private T mCurrentItem = null; |
| |
| /** |
| * Constructs a new touch exploration helper. |
| * |
| * @param context The parent context. |
| */ |
| public TouchExplorationHelper(Context context, View parentView) { |
| mManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); |
| mParentView = parentView; |
| } |
| |
| /** |
| * @return The current accessibility focused item, or {@code null} if no |
| * item is focused. |
| */ |
| public T getFocusedItem() { |
| return getItemForId(mFocusedItemId); |
| } |
| |
| /** |
| * Clears the current accessibility focused item. |
| */ |
| public void clearFocusedItem() { |
| final int itemId = mFocusedItemId; |
| if (itemId == INVALID_ID) { |
| return; |
| } |
| |
| performAction(itemId, AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); |
| } |
| |
| /** |
| * Requests accessibility focus be placed on the specified item. |
| * |
| * @param item The item to place focus on. |
| */ |
| public void setFocusedItem(T item) { |
| final int itemId = getIdForItem(item); |
| if (itemId == INVALID_ID) { |
| return; |
| } |
| |
| performAction(itemId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); |
| } |
| |
| /** |
| * Invalidates cached information about the parent view. |
| * <p> |
| * You <b>must</b> call this method after adding or removing items from the |
| * parent view. |
| * </p> |
| */ |
| public void invalidateParent() { |
| mParentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); |
| } |
| |
| /** |
| * Invalidates cached information for a particular item. |
| * <p> |
| * You <b>must</b> call this method when any of the properties set in |
| * {@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)} have |
| * changed. |
| * </p> |
| * |
| * @param item |
| */ |
| public void invalidateItem(T item) { |
| sendEventForItem(item, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); |
| } |
| |
| /** |
| * Populates an event of the specified type with information about an item |
| * and attempts to send it up through the view hierarchy. |
| * |
| * @param item The item for which to send an event. |
| * @param eventType The type of event to send. |
| * @return {@code true} if the event was sent successfully. |
| */ |
| public boolean sendEventForItem(T item, int eventType) { |
| if (!mManager.isEnabled()) { |
| return false; |
| } |
| |
| final AccessibilityEvent event = getEventForItem(item, eventType); |
| final ViewGroup group = (ViewGroup) mParentView.getParent(); |
| |
| return group.requestSendAccessibilityEvent(mParentView, event); |
| } |
| |
| @Override |
| public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) { |
| if (virtualViewId == View.NO_ID) { |
| return getNodeForParent(); |
| } |
| |
| final T item = getItemForId(virtualViewId); |
| if (item == null) { |
| return null; |
| } |
| |
| final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain(); |
| populateNodeForItemInternal(item, node); |
| return node; |
| } |
| |
| @Override |
| public boolean performAction(int virtualViewId, int action, Bundle arguments) { |
| if (virtualViewId == View.NO_ID) { |
| return ViewCompat.performAccessibilityAction(mParentView, action, arguments); |
| } |
| |
| final T item = getItemForId(virtualViewId); |
| if (item == null) { |
| return false; |
| } |
| |
| boolean handled = false; |
| |
| switch (action) { |
| case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: |
| if (mFocusedItemId != virtualViewId) { |
| mFocusedItemId = virtualViewId; |
| sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); |
| handled = true; |
| } |
| break; |
| case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: |
| if (mFocusedItemId == virtualViewId) { |
| mFocusedItemId = INVALID_ID; |
| sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); |
| handled = true; |
| } |
| break; |
| } |
| |
| handled |= performActionForItem(item, action, arguments); |
| |
| return handled; |
| } |
| |
| @Override |
| public boolean onHover(View view, MotionEvent event) { |
| if (!mManager.isTouchExplorationEnabled()) { |
| return false; |
| } |
| |
| switch (event.getAction()) { |
| case MotionEvent.ACTION_HOVER_ENTER: |
| case MotionEvent.ACTION_HOVER_MOVE: |
| final T item = getItemAt(event.getX(), event.getY()); |
| setCurrentItem(item); |
| return true; |
| case MotionEvent.ACTION_HOVER_EXIT: |
| setCurrentItem(null); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private void setCurrentItem(T item) { |
| if (mCurrentItem == item) { |
| return; |
| } |
| |
| if (mCurrentItem != null) { |
| sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); |
| } |
| |
| mCurrentItem = item; |
| |
| if (mCurrentItem != null) { |
| sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); |
| } |
| } |
| |
| private AccessibilityEvent getEventForItem(T item, int eventType) { |
| final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); |
| final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event); |
| final int virtualDescendantId = getIdForItem(item); |
| |
| // Ensure the client has good defaults. |
| event.setEnabled(true); |
| |
| // Allow the client to populate the event. |
| populateEventForItem(item, event); |
| |
| if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) { |
| throw new RuntimeException( |
| "You must add text or a content description in populateEventForItem()"); |
| } |
| |
| // Don't allow the client to override these properties. |
| event.setClassName(item.getClass().getName()); |
| event.setPackageName(mParentView.getContext().getPackageName()); |
| record.setSource(mParentView, virtualDescendantId); |
| |
| return event; |
| } |
| |
| private AccessibilityNodeInfoCompat getNodeForParent() { |
| final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(mParentView); |
| ViewCompat.onInitializeAccessibilityNodeInfo(mParentView, info); |
| |
| final LinkedList<T> items = new LinkedList<T>(); |
| getVisibleItems(items); |
| |
| for (T item : items) { |
| final int virtualDescendantId = getIdForItem(item); |
| info.addChild(mParentView, virtualDescendantId); |
| } |
| |
| return info; |
| } |
| |
| private AccessibilityNodeInfoCompat populateNodeForItemInternal( |
| T item, AccessibilityNodeInfoCompat node) { |
| final int virtualDescendantId = getIdForItem(item); |
| |
| // Ensure the client has good defaults. |
| node.setEnabled(true); |
| |
| // Allow the client to populate the node. |
| populateNodeForItem(item, node); |
| |
| if (TextUtils.isEmpty(node.getText()) && TextUtils.isEmpty(node.getContentDescription())) { |
| throw new RuntimeException( |
| "You must add text or a content description in populateNodeForItem()"); |
| } |
| |
| // Don't allow the client to override these properties. |
| node.setPackageName(mParentView.getContext().getPackageName()); |
| node.setClassName(item.getClass().getName()); |
| node.setParent(mParentView); |
| node.setSource(mParentView, virtualDescendantId); |
| |
| if (mFocusedItemId == virtualDescendantId) { |
| node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); |
| } else { |
| node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); |
| } |
| |
| node.getBoundsInParent(mTempParentRect); |
| if (mTempParentRect.isEmpty()) { |
| throw new RuntimeException("You must set parent bounds in populateNodeForItem()"); |
| } |
| |
| // Set the visibility based on the parent bound. |
| if (intersectVisibleToUser(mTempParentRect)) { |
| node.setVisibleToUser(true); |
| node.setBoundsInParent(mTempParentRect); |
| } |
| |
| // Calculate screen-relative bound. |
| mParentView.getLocationOnScreen(mTempGlobalRect); |
| final int offsetX = mTempGlobalRect[0]; |
| final int offsetY = mTempGlobalRect[1]; |
| mTempScreenRect.set(mTempParentRect); |
| mTempScreenRect.offset(offsetX, offsetY); |
| node.setBoundsInScreen(mTempScreenRect); |
| |
| return node; |
| } |
| |
| /** |
| * Computes whether the specified {@link Rect} intersects with the visible |
| * portion of its parent {@link View}. Modifies {@code localRect} to |
| * contain only the visible portion. |
| * |
| * @param localRect A rectangle in local (parent) coordinates. |
| * @return Whether the specified {@link Rect} is visible on the screen. |
| */ |
| private boolean intersectVisibleToUser(Rect localRect) { |
| // Missing or empty bounds mean this view is not visible. |
| if ((localRect == null) || localRect.isEmpty()) { |
| return false; |
| } |
| |
| // Attached to invisible window means this view is not visible. |
| if (mParentView.getWindowVisibility() != View.VISIBLE) { |
| return false; |
| } |
| |
| // An invisible predecessor or one with alpha zero means |
| // that this view is not visible to the user. |
| Object current = this; |
| while (current instanceof View) { |
| final View view = (View) current; |
| // We have attach info so this view is attached and there is no |
| // need to check whether we reach to ViewRootImpl on the way up. |
| if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) { |
| return false; |
| } |
| current = view.getParent(); |
| } |
| |
| // If no portion of the parent is visible, this view is not visible. |
| if (!mParentView.getLocalVisibleRect(mTempVisibleRect)) { |
| return false; |
| } |
| |
| // Check if the view intersects the visible portion of the parent. |
| return localRect.intersect(mTempVisibleRect); |
| } |
| |
| public AccessibilityDelegateCompat getAccessibilityDelegate() { |
| return mDelegate; |
| } |
| |
| private final AccessibilityDelegateCompat mDelegate = new AccessibilityDelegateCompat() { |
| @Override |
| public void onInitializeAccessibilityEvent(View view, AccessibilityEvent event) { |
| super.onInitializeAccessibilityEvent(view, event); |
| event.setClassName(view.getClass().getName()); |
| } |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfoCompat info) { |
| super.onInitializeAccessibilityNodeInfo(view, info); |
| info.setClassName(view.getClass().getName()); |
| } |
| |
| @Override |
| public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { |
| return TouchExplorationHelper.this; |
| } |
| }; |
| |
| /** |
| * Performs an accessibility action on the specified item. See |
| * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)}. |
| * <p> |
| * The helper class automatically handles focus management resulting from |
| * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} and |
| * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS}, so |
| * typically a developer only needs to handle actions added manually in the |
| * {{@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)} |
| * method. |
| * </p> |
| * |
| * @param item The item on which to perform the action. |
| * @param action The accessibility action to perform. |
| * @param arguments Arguments for the action, or optionally {@code null}. |
| * @return {@code true} if the action was performed successfully. |
| */ |
| protected abstract boolean performActionForItem(T item, int action, Bundle arguments); |
| |
| /** |
| * Populates an event with information about the specified item. |
| * <p> |
| * At a minimum, a developer must populate the event text by doing one of |
| * the following: |
| * <ul> |
| * <li>appending text to {@link AccessibilityEvent#getText()}</li> |
| * <li>populating a description with |
| * {@link AccessibilityEvent#setContentDescription(CharSequence)}</li> |
| * </ul> |
| * </p> |
| * |
| * @param item The item for which to populate the event. |
| * @param event The event to populate. |
| */ |
| protected abstract void populateEventForItem(T item, AccessibilityEvent event); |
| |
| /** |
| * Populates a node with information about the specified item. |
| * <p> |
| * At a minimum, a developer must: |
| * <ul> |
| * <li>populate the event text using |
| * {@link AccessibilityNodeInfoCompat#setText(CharSequence)} or |
| * {@link AccessibilityNodeInfoCompat#setContentDescription(CharSequence)} |
| * </li> |
| * <li>set the item's parent-relative bounds using |
| * {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)} |
| * </ul> |
| * |
| * @param item The item for which to populate the node. |
| * @param node The node to populate. |
| */ |
| protected abstract void populateNodeForItem(T item, AccessibilityNodeInfoCompat node); |
| |
| /** |
| * Populates a list with the parent view's visible items. |
| * <p> |
| * The result of this method is cached until the developer calls |
| * {@link #invalidateParent()}. |
| * </p> |
| * |
| * @param items The list to populate with visible items. |
| */ |
| protected abstract void getVisibleItems(List<T> items); |
| |
| /** |
| * Returns the item under the specified parent-relative coordinates. |
| * |
| * @param x The parent-relative x coordinate. |
| * @param y The parent-relative y coordinate. |
| * @return The item under coordinates (x,y). |
| */ |
| protected abstract T getItemAt(float x, float y); |
| |
| /** |
| * Returns the unique identifier for an item. If the specified item does not |
| * exist, returns {@link #INVALID_ID}. |
| * <p> |
| * This result of this method must be consistent with |
| * {@link #getItemForId(int)}. |
| * </p> |
| * |
| * @param item The item whose identifier to return. |
| * @return A unique identifier, or {@link #INVALID_ID}. |
| */ |
| protected abstract int getIdForItem(T item); |
| |
| /** |
| * Returns the item for a unique identifier. If the specified item does not |
| * exist, returns {@code null}. |
| * |
| * @param id The identifier for the item to return. |
| * @return An item, or {@code null}. |
| */ |
| protected abstract T getItemForId(int id); |
| } |