Home | History | Annotate | Download | only in utils
      1 /*
      2  * Copyright (C) 2012 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package com.googlecode.eyesfree.utils;
     18 
     19 import android.content.Context;
     20 import android.graphics.Rect;
     21 import android.os.Bundle;
     22 import android.support.v4.view.AccessibilityDelegateCompat;
     23 import android.support.v4.view.ViewCompat;
     24 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     25 import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
     26 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
     27 import android.text.TextUtils;
     28 import android.view.MotionEvent;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 import android.view.accessibility.AccessibilityEvent;
     32 import android.view.accessibility.AccessibilityManager;
     33 
     34 import java.util.LinkedList;
     35 import java.util.List;
     36 
     37 public abstract class TouchExplorationHelper<T> extends AccessibilityNodeProviderCompat
     38         implements View.OnHoverListener {
     39     /** Virtual node identifier value for invalid nodes. */
     40     public static final int INVALID_ID = Integer.MIN_VALUE;
     41 
     42     private final Rect mTempScreenRect = new Rect();
     43     private final Rect mTempParentRect = new Rect();
     44     private final Rect mTempVisibleRect = new Rect();
     45     private final int[] mTempGlobalRect = new int[2];
     46 
     47     private final AccessibilityManager mManager;
     48 
     49     private View mParentView;
     50     private int mFocusedItemId = INVALID_ID;
     51     private T mCurrentItem = null;
     52 
     53     /**
     54      * Constructs a new touch exploration helper.
     55      *
     56      * @param context The parent context.
     57      */
     58     public TouchExplorationHelper(Context context, View parentView) {
     59         mManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
     60         mParentView = parentView;
     61     }
     62 
     63     /**
     64      * @return The current accessibility focused item, or {@code null} if no
     65      *         item is focused.
     66      */
     67     public T getFocusedItem() {
     68         return getItemForId(mFocusedItemId);
     69     }
     70 
     71     /**
     72      * Clears the current accessibility focused item.
     73      */
     74     public void clearFocusedItem() {
     75         final int itemId = mFocusedItemId;
     76         if (itemId == INVALID_ID) {
     77             return;
     78         }
     79 
     80         performAction(itemId, AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
     81     }
     82 
     83     /**
     84      * Requests accessibility focus be placed on the specified item.
     85      *
     86      * @param item The item to place focus on.
     87      */
     88     public void setFocusedItem(T item) {
     89         final int itemId = getIdForItem(item);
     90         if (itemId == INVALID_ID) {
     91             return;
     92         }
     93 
     94         performAction(itemId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
     95     }
     96 
     97     /**
     98      * Invalidates cached information about the parent view.
     99      * <p>
    100      * You <b>must</b> call this method after adding or removing items from the
    101      * parent view.
    102      * </p>
    103      */
    104     public void invalidateParent() {
    105         mParentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    106     }
    107 
    108     /**
    109      * Invalidates cached information for a particular item.
    110      * <p>
    111      * You <b>must</b> call this method when any of the properties set in
    112      * {@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)} have
    113      * changed.
    114      * </p>
    115      *
    116      * @param item
    117      */
    118     public void invalidateItem(T item) {
    119         sendEventForItem(item, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    120     }
    121 
    122     /**
    123      * Populates an event of the specified type with information about an item
    124      * and attempts to send it up through the view hierarchy.
    125      *
    126      * @param item The item for which to send an event.
    127      * @param eventType The type of event to send.
    128      * @return {@code true} if the event was sent successfully.
    129      */
    130     public boolean sendEventForItem(T item, int eventType) {
    131         if (!mManager.isEnabled()) {
    132             return false;
    133         }
    134 
    135         final AccessibilityEvent event = getEventForItem(item, eventType);
    136         final ViewGroup group = (ViewGroup) mParentView.getParent();
    137 
    138         return group.requestSendAccessibilityEvent(mParentView, event);
    139     }
    140 
    141     @Override
    142     public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
    143         if (virtualViewId == View.NO_ID) {
    144             return getNodeForParent();
    145         }
    146 
    147         final T item = getItemForId(virtualViewId);
    148         if (item == null) {
    149             return null;
    150         }
    151 
    152         final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain();
    153         populateNodeForItemInternal(item, node);
    154         return node;
    155     }
    156 
    157     @Override
    158     public boolean performAction(int virtualViewId, int action, Bundle arguments) {
    159         if (virtualViewId == View.NO_ID) {
    160             return ViewCompat.performAccessibilityAction(mParentView, action, arguments);
    161         }
    162 
    163         final T item = getItemForId(virtualViewId);
    164         if (item == null) {
    165             return false;
    166         }
    167 
    168         boolean handled = false;
    169 
    170         switch (action) {
    171             case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
    172                 if (mFocusedItemId != virtualViewId) {
    173                     mFocusedItemId = virtualViewId;
    174                     sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
    175                     handled = true;
    176                 }
    177                 break;
    178             case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
    179                 if (mFocusedItemId == virtualViewId) {
    180                     mFocusedItemId = INVALID_ID;
    181                     sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
    182                     handled = true;
    183                 }
    184                 break;
    185         }
    186 
    187         handled |= performActionForItem(item, action, arguments);
    188 
    189         return handled;
    190     }
    191 
    192     @Override
    193     public boolean onHover(View view, MotionEvent event) {
    194         if (!mManager.isTouchExplorationEnabled()) {
    195             return false;
    196         }
    197 
    198         switch (event.getAction()) {
    199             case MotionEvent.ACTION_HOVER_ENTER:
    200             case MotionEvent.ACTION_HOVER_MOVE:
    201                 final T item = getItemAt(event.getX(), event.getY());
    202                 setCurrentItem(item);
    203                 return true;
    204             case MotionEvent.ACTION_HOVER_EXIT:
    205                 setCurrentItem(null);
    206                 return true;
    207         }
    208 
    209         return false;
    210     }
    211 
    212     private void setCurrentItem(T item) {
    213         if (mCurrentItem == item) {
    214             return;
    215         }
    216 
    217         if (mCurrentItem != null) {
    218             sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
    219         }
    220 
    221         mCurrentItem = item;
    222 
    223         if (mCurrentItem != null) {
    224             sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
    225         }
    226     }
    227 
    228     private AccessibilityEvent getEventForItem(T item, int eventType) {
    229         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
    230         final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
    231         final int virtualDescendantId = getIdForItem(item);
    232 
    233         // Ensure the client has good defaults.
    234         event.setEnabled(true);
    235 
    236         // Allow the client to populate the event.
    237         populateEventForItem(item, event);
    238 
    239         if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) {
    240             throw new RuntimeException(
    241                     "You must add text or a content description in populateEventForItem()");
    242         }
    243 
    244         // Don't allow the client to override these properties.
    245         event.setClassName(item.getClass().getName());
    246         event.setPackageName(mParentView.getContext().getPackageName());
    247         record.setSource(mParentView, virtualDescendantId);
    248 
    249         return event;
    250     }
    251 
    252     private AccessibilityNodeInfoCompat getNodeForParent() {
    253         final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(mParentView);
    254         ViewCompat.onInitializeAccessibilityNodeInfo(mParentView, info);
    255 
    256         final LinkedList<T> items = new LinkedList<T>();
    257         getVisibleItems(items);
    258 
    259         for (T item : items) {
    260             final int virtualDescendantId = getIdForItem(item);
    261             info.addChild(mParentView, virtualDescendantId);
    262         }
    263 
    264         return info;
    265     }
    266 
    267     private AccessibilityNodeInfoCompat populateNodeForItemInternal(
    268             T item, AccessibilityNodeInfoCompat node) {
    269         final int virtualDescendantId = getIdForItem(item);
    270 
    271         // Ensure the client has good defaults.
    272         node.setEnabled(true);
    273 
    274         // Allow the client to populate the node.
    275         populateNodeForItem(item, node);
    276 
    277         if (TextUtils.isEmpty(node.getText()) && TextUtils.isEmpty(node.getContentDescription())) {
    278             throw new RuntimeException(
    279                     "You must add text or a content description in populateNodeForItem()");
    280         }
    281 
    282         // Don't allow the client to override these properties.
    283         node.setPackageName(mParentView.getContext().getPackageName());
    284         node.setClassName(item.getClass().getName());
    285         node.setParent(mParentView);
    286         node.setSource(mParentView, virtualDescendantId);
    287 
    288         if (mFocusedItemId == virtualDescendantId) {
    289             node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
    290         } else {
    291             node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
    292         }
    293 
    294         node.getBoundsInParent(mTempParentRect);
    295         if (mTempParentRect.isEmpty()) {
    296             throw new RuntimeException("You must set parent bounds in populateNodeForItem()");
    297         }
    298 
    299         // Set the visibility based on the parent bound.
    300         if (intersectVisibleToUser(mTempParentRect)) {
    301             node.setVisibleToUser(true);
    302             node.setBoundsInParent(mTempParentRect);
    303         }
    304 
    305         // Calculate screen-relative bound.
    306         mParentView.getLocationOnScreen(mTempGlobalRect);
    307         final int offsetX = mTempGlobalRect[0];
    308         final int offsetY = mTempGlobalRect[1];
    309         mTempScreenRect.set(mTempParentRect);
    310         mTempScreenRect.offset(offsetX, offsetY);
    311         node.setBoundsInScreen(mTempScreenRect);
    312 
    313         return node;
    314     }
    315 
    316     /**
    317      * Computes whether the specified {@link Rect} intersects with the visible
    318      * portion of its parent {@link View}. Modifies {@code localRect} to
    319      * contain only the visible portion.
    320      *
    321      * @param localRect A rectangle in local (parent) coordinates.
    322      * @return Whether the specified {@link Rect} is visible on the screen.
    323      */
    324     private boolean intersectVisibleToUser(Rect localRect) {
    325         // Missing or empty bounds mean this view is not visible.
    326         if ((localRect == null) || localRect.isEmpty()) {
    327             return false;
    328         }
    329 
    330         // Attached to invisible window means this view is not visible.
    331         if (mParentView.getWindowVisibility() != View.VISIBLE) {
    332             return false;
    333         }
    334 
    335         // An invisible predecessor or one with alpha zero means
    336         // that this view is not visible to the user.
    337         Object current = this;
    338         while (current instanceof View) {
    339             final View view = (View) current;
    340             // We have attach info so this view is attached and there is no
    341             // need to check whether we reach to ViewRootImpl on the way up.
    342             if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) {
    343                 return false;
    344             }
    345             current = view.getParent();
    346         }
    347 
    348         // If no portion of the parent is visible, this view is not visible.
    349         if (!mParentView.getLocalVisibleRect(mTempVisibleRect)) {
    350             return false;
    351         }
    352 
    353         // Check if the view intersects the visible portion of the parent.
    354         return localRect.intersect(mTempVisibleRect);
    355     }
    356 
    357     public AccessibilityDelegateCompat getAccessibilityDelegate() {
    358         return mDelegate;
    359     }
    360 
    361     private final AccessibilityDelegateCompat mDelegate = new AccessibilityDelegateCompat() {
    362         @Override
    363         public void onInitializeAccessibilityEvent(View view, AccessibilityEvent event) {
    364             super.onInitializeAccessibilityEvent(view, event);
    365             event.setClassName(view.getClass().getName());
    366         }
    367 
    368         @Override
    369         public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfoCompat info) {
    370             super.onInitializeAccessibilityNodeInfo(view, info);
    371             info.setClassName(view.getClass().getName());
    372         }
    373 
    374         @Override
    375         public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
    376             return TouchExplorationHelper.this;
    377         }
    378     };
    379 
    380     /**
    381      * Performs an accessibility action on the specified item. See
    382      * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)}.
    383      * <p>
    384      * The helper class automatically handles focus management resulting from
    385      * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} and
    386      * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS}, so
    387      * typically a developer only needs to handle actions added manually in the
    388      * {{@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)}
    389      * method.
    390      * </p>
    391      *
    392      * @param item The item on which to perform the action.
    393      * @param action The accessibility action to perform.
    394      * @param arguments Arguments for the action, or optionally {@code null}.
    395      * @return {@code true} if the action was performed successfully.
    396      */
    397     protected abstract boolean performActionForItem(T item, int action, Bundle arguments);
    398 
    399     /**
    400      * Populates an event with information about the specified item.
    401      * <p>
    402      * At a minimum, a developer must populate the event text by doing one of
    403      * the following:
    404      * <ul>
    405      * <li>appending text to {@link AccessibilityEvent#getText()}</li>
    406      * <li>populating a description with
    407      * {@link AccessibilityEvent#setContentDescription(CharSequence)}</li>
    408      * </ul>
    409      * </p>
    410      *
    411      * @param item The item for which to populate the event.
    412      * @param event The event to populate.
    413      */
    414     protected abstract void populateEventForItem(T item, AccessibilityEvent event);
    415 
    416     /**
    417      * Populates a node with information about the specified item.
    418      * <p>
    419      * At a minimum, a developer must:
    420      * <ul>
    421      * <li>populate the event text using
    422      * {@link AccessibilityNodeInfoCompat#setText(CharSequence)} or
    423      * {@link AccessibilityNodeInfoCompat#setContentDescription(CharSequence)}
    424      * </li>
    425      * <li>set the item's parent-relative bounds using
    426      * {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)}
    427      * </ul>
    428      *
    429      * @param item The item for which to populate the node.
    430      * @param node The node to populate.
    431      */
    432     protected abstract void populateNodeForItem(T item, AccessibilityNodeInfoCompat node);
    433 
    434     /**
    435      * Populates a list with the parent view's visible items.
    436      * <p>
    437      * The result of this method is cached until the developer calls
    438      * {@link #invalidateParent()}.
    439      * </p>
    440      *
    441      * @param items The list to populate with visible items.
    442      */
    443     protected abstract void getVisibleItems(List<T> items);
    444 
    445     /**
    446      * Returns the item under the specified parent-relative coordinates.
    447      *
    448      * @param x The parent-relative x coordinate.
    449      * @param y The parent-relative y coordinate.
    450      * @return The item under coordinates (x,y).
    451      */
    452     protected abstract T getItemAt(float x, float y);
    453 
    454     /**
    455      * Returns the unique identifier for an item. If the specified item does not
    456      * exist, returns {@link #INVALID_ID}.
    457      * <p>
    458      * This result of this method must be consistent with
    459      * {@link #getItemForId(int)}.
    460      * </p>
    461      *
    462      * @param item The item whose identifier to return.
    463      * @return A unique identifier, or {@link #INVALID_ID}.
    464      */
    465     protected abstract int getIdForItem(T item);
    466 
    467     /**
    468      * Returns the item for a unique identifier. If the specified item does not
    469      * exist, returns {@code null}.
    470      *
    471      * @param id The identifier for the item to return.
    472      * @return An item, or {@code null}.
    473      */
    474     protected abstract T getItemForId(int id);
    475 }
    476