Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of 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,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.internal.widget;
     18 
     19 import android.content.Context;
     20 import android.graphics.Rect;
     21 import android.os.Bundle;
     22 import android.util.IntArray;
     23 import android.view.MotionEvent;
     24 import android.view.View;
     25 import android.view.ViewParent;
     26 import android.view.accessibility.AccessibilityEvent;
     27 import android.view.accessibility.AccessibilityManager;
     28 import android.view.accessibility.AccessibilityNodeInfo;
     29 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
     30 import android.view.accessibility.AccessibilityNodeProvider;
     31 
     32 /**
     33  * ExploreByTouchHelper is a utility class for implementing accessibility
     34  * support in custom {@link android.view.View}s that represent a collection of View-like
     35  * logical items. It extends {@link android.view.accessibility.AccessibilityNodeProvider} and
     36  * simplifies many aspects of providing information to accessibility services
     37  * and managing accessibility focus. This class does not currently support
     38  * hierarchies of logical items.
     39  * <p>
     40  * This should be applied to the parent view using
     41  * {@link android.view.View#setAccessibilityDelegate}:
     42  *
     43  * <pre>
     44  * mAccessHelper = ExploreByTouchHelper.create(someView, mAccessHelperCallback);
     45  * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper);
     46  * </pre>
     47  */
     48 public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {
     49     /** Virtual node identifier value for invalid nodes. */
     50     public static final int INVALID_ID = Integer.MIN_VALUE;
     51 
     52     /** Virtual node identifier value for the host view's node. */
     53     public static final int HOST_ID = View.NO_ID;
     54 
     55     /** Default class name used for virtual views. */
     56     private static final String DEFAULT_CLASS_NAME = View.class.getName();
     57 
     58     /** Default bounds used to determine if the client didn't set any. */
     59     private static final Rect INVALID_PARENT_BOUNDS = new Rect(
     60             Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
     61 
     62     // Lazily-created temporary data structures used when creating nodes.
     63     private Rect mTempScreenRect;
     64     private Rect mTempParentRect;
     65     private int[] mTempGlobalRect;
     66 
     67     /** Lazily-created temporary data structure used to compute visibility. */
     68     private Rect mTempVisibleRect;
     69 
     70     /** Lazily-created temporary data structure used to obtain child IDs. */
     71     private IntArray mTempArray;
     72 
     73     /** System accessibility manager, used to check state and send events. */
     74     private final AccessibilityManager mManager;
     75 
     76     /** View whose internal structure is exposed through this helper. */
     77     private final View mView;
     78 
     79     /** Context of the host view. **/
     80     private final Context mContext;
     81 
     82     /** Node provider that handles creating nodes and performing actions. */
     83     private ExploreByTouchNodeProvider mNodeProvider;
     84 
     85     /** Virtual view id for the currently focused logical item. */
     86     private int mFocusedVirtualViewId = INVALID_ID;
     87 
     88     /** Virtual view id for the currently hovered logical item. */
     89     private int mHoveredVirtualViewId = INVALID_ID;
     90 
     91     /**
     92      * Factory method to create a new {@link ExploreByTouchHelper}.
     93      *
     94      * @param forView View whose logical children are exposed by this helper.
     95      */
     96     public ExploreByTouchHelper(View forView) {
     97         if (forView == null) {
     98             throw new IllegalArgumentException("View may not be null");
     99         }
    100 
    101         mView = forView;
    102         mContext = forView.getContext();
    103         mManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
    104     }
    105 
    106     /**
    107      * Returns the {@link android.view.accessibility.AccessibilityNodeProvider} for this helper.
    108      *
    109      * @param host View whose logical children are exposed by this helper.
    110      * @return The accessibility node provider for this helper.
    111      */
    112     @Override
    113     public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
    114         if (mNodeProvider == null) {
    115             mNodeProvider = new ExploreByTouchNodeProvider();
    116         }
    117         return mNodeProvider;
    118     }
    119 
    120     /**
    121      * Dispatches hover {@link android.view.MotionEvent}s to the virtual view hierarchy when
    122      * the Explore by Touch feature is enabled.
    123      * <p>
    124      * This method should be called by overriding
    125      * {@link View#dispatchHoverEvent}:
    126      *
    127      * <pre>&#64;Override
    128      * public boolean dispatchHoverEvent(MotionEvent event) {
    129      *   if (mHelper.dispatchHoverEvent(this, event) {
    130      *     return true;
    131      *   }
    132      *   return super.dispatchHoverEvent(event);
    133      * }
    134      * </pre>
    135      *
    136      * @param event The hover event to dispatch to the virtual view hierarchy.
    137      * @return Whether the hover event was handled.
    138      */
    139     public boolean dispatchHoverEvent(MotionEvent event) {
    140         if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) {
    141             return false;
    142         }
    143 
    144         switch (event.getAction()) {
    145             case MotionEvent.ACTION_HOVER_MOVE:
    146             case MotionEvent.ACTION_HOVER_ENTER:
    147                 final int virtualViewId = getVirtualViewAt(event.getX(), event.getY());
    148                 updateHoveredVirtualView(virtualViewId);
    149                 return (virtualViewId != INVALID_ID);
    150             case MotionEvent.ACTION_HOVER_EXIT:
    151                 if (mFocusedVirtualViewId != INVALID_ID) {
    152                     updateHoveredVirtualView(INVALID_ID);
    153                     return true;
    154                 }
    155                 return false;
    156             default:
    157                 return false;
    158         }
    159     }
    160 
    161     /**
    162      * Populates an event of the specified type with information about an item
    163      * and attempts to send it up through the view hierarchy.
    164      * <p>
    165      * You should call this method after performing a user action that normally
    166      * fires an accessibility event, such as clicking on an item.
    167      *
    168      * <pre>public void performItemClick(T item) {
    169      *   ...
    170      *   sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED);
    171      * }
    172      * </pre>
    173      *
    174      * @param virtualViewId The virtual view id for which to send an event.
    175      * @param eventType The type of event to send.
    176      * @return true if the event was sent successfully.
    177      */
    178     public boolean sendEventForVirtualView(int virtualViewId, int eventType) {
    179         if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) {
    180             return false;
    181         }
    182 
    183         final ViewParent parent = mView.getParent();
    184         if (parent == null) {
    185             return false;
    186         }
    187 
    188         final AccessibilityEvent event = createEvent(virtualViewId, eventType);
    189         return parent.requestSendAccessibilityEvent(mView, event);
    190     }
    191 
    192     /**
    193      * Notifies the accessibility framework that the properties of the parent
    194      * view have changed.
    195      * <p>
    196      * You <b>must</b> call this method after adding or removing items from the
    197      * parent view.
    198      */
    199     public void invalidateRoot() {
    200         invalidateVirtualView(HOST_ID, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
    201     }
    202 
    203     /**
    204      * Notifies the accessibility framework that the properties of a particular
    205      * item have changed.
    206      * <p>
    207      * You <b>must</b> call this method after changing any of the properties set
    208      * in {@link #onPopulateNodeForVirtualView}.
    209      *
    210      * @param virtualViewId The virtual view id to invalidate, or
    211      *                      {@link #HOST_ID} to invalidate the root view.
    212      * @see #invalidateVirtualView(int, int)
    213      */
    214     public void invalidateVirtualView(int virtualViewId) {
    215         invalidateVirtualView(virtualViewId,
    216                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
    217     }
    218 
    219     /**
    220      * Notifies the accessibility framework that the properties of a particular
    221      * item have changed.
    222      * <p>
    223      * You <b>must</b> call this method after changing any of the properties set
    224      * in {@link #onPopulateNodeForVirtualView}.
    225      *
    226      * @param virtualViewId The virtual view id to invalidate, or
    227      *                      {@link #HOST_ID} to invalidate the root view.
    228      * @param changeTypes The bit mask of change types. May be {@code 0} for the
    229      *                    default (undefined) change type or one or more of:
    230      *         <ul>
    231      *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION}
    232      *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
    233      *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
    234      *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
    235      *         </ul>
    236      */
    237     public void invalidateVirtualView(int virtualViewId, int changeTypes) {
    238         if (virtualViewId != INVALID_ID && mManager.isEnabled()) {
    239             final ViewParent parent = mView.getParent();
    240             if (parent != null) {
    241                 final AccessibilityEvent event = createEvent(virtualViewId,
    242                         AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    243                 event.setContentChangeTypes(changeTypes);
    244                 parent.requestSendAccessibilityEvent(mView, event);
    245             }
    246         }
    247     }
    248 
    249     /**
    250      * Returns the virtual view id for the currently focused item,
    251      *
    252      * @return A virtual view id, or {@link #INVALID_ID} if no item is
    253      *         currently focused.
    254      */
    255     public int getFocusedVirtualView() {
    256         return mFocusedVirtualViewId;
    257     }
    258 
    259     /**
    260      * Sets the currently hovered item, sending hover accessibility events as
    261      * necessary to maintain the correct state.
    262      *
    263      * @param virtualViewId The virtual view id for the item currently being
    264      *            hovered, or {@link #INVALID_ID} if no item is hovered within
    265      *            the parent view.
    266      */
    267     private void updateHoveredVirtualView(int virtualViewId) {
    268         if (mHoveredVirtualViewId == virtualViewId) {
    269             return;
    270         }
    271 
    272         final int previousVirtualViewId = mHoveredVirtualViewId;
    273         mHoveredVirtualViewId = virtualViewId;
    274 
    275         // Stay consistent with framework behavior by sending ENTER/EXIT pairs
    276         // in reverse order. This is accurate as of API 18.
    277         sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
    278         sendEventForVirtualView(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
    279     }
    280 
    281     /**
    282      * Constructs and returns an {@link AccessibilityEvent} for the specified
    283      * virtual view id, which includes the host view ({@link #HOST_ID}).
    284      *
    285      * @param virtualViewId The virtual view id for the item for which to
    286      *            construct an event.
    287      * @param eventType The type of event to construct.
    288      * @return An {@link AccessibilityEvent} populated with information about
    289      *         the specified item.
    290      */
    291     private AccessibilityEvent createEvent(int virtualViewId, int eventType) {
    292         switch (virtualViewId) {
    293             case HOST_ID:
    294                 return createEventForHost(eventType);
    295             default:
    296                 return createEventForChild(virtualViewId, eventType);
    297         }
    298     }
    299 
    300     /**
    301      * Constructs and returns an {@link AccessibilityEvent} for the host node.
    302      *
    303      * @param eventType The type of event to construct.
    304      * @return An {@link AccessibilityEvent} populated with information about
    305      *         the specified item.
    306      */
    307     private AccessibilityEvent createEventForHost(int eventType) {
    308         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
    309         mView.onInitializeAccessibilityEvent(event);
    310 
    311         // Allow the client to populate the event.
    312         onPopulateEventForHost(event);
    313 
    314         return event;
    315     }
    316 
    317     /**
    318      * Constructs and returns an {@link AccessibilityEvent} populated with
    319      * information about the specified item.
    320      *
    321      * @param virtualViewId The virtual view id for the item for which to
    322      *            construct an event.
    323      * @param eventType The type of event to construct.
    324      * @return An {@link AccessibilityEvent} populated with information about
    325      *         the specified item.
    326      */
    327     private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) {
    328         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
    329         event.setEnabled(true);
    330         event.setClassName(DEFAULT_CLASS_NAME);
    331 
    332         // Allow the client to populate the event.
    333         onPopulateEventForVirtualView(virtualViewId, event);
    334 
    335         // Make sure the developer is following the rules.
    336         if (event.getText().isEmpty() && (event.getContentDescription() == null)) {
    337             throw new RuntimeException("Callbacks must add text or a content description in "
    338                     + "populateEventForVirtualViewId()");
    339         }
    340 
    341         // Don't allow the client to override these properties.
    342         event.setPackageName(mView.getContext().getPackageName());
    343         event.setSource(mView, virtualViewId);
    344 
    345         return event;
    346     }
    347 
    348     /**
    349      * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the
    350      * specified virtual view id, which includes the host view
    351      * ({@link #HOST_ID}).
    352      *
    353      * @param virtualViewId The virtual view id for the item for which to
    354      *            construct a node.
    355      * @return An {@link android.view.accessibility.AccessibilityNodeInfo} populated with information
    356      *         about the specified item.
    357      */
    358     private AccessibilityNodeInfo createNode(int virtualViewId) {
    359         switch (virtualViewId) {
    360             case HOST_ID:
    361                 return createNodeForHost();
    362             default:
    363                 return createNodeForChild(virtualViewId);
    364         }
    365     }
    366 
    367     /**
    368      * Constructs and returns an {@link AccessibilityNodeInfo} for the
    369      * host view populated with its virtual descendants.
    370      *
    371      * @return An {@link AccessibilityNodeInfo} for the parent node.
    372      */
    373     private AccessibilityNodeInfo createNodeForHost() {
    374         final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView);
    375         mView.onInitializeAccessibilityNodeInfo(node);
    376         final int realNodeCount = node.getChildCount();
    377 
    378         // Allow the client to populate the host node.
    379         onPopulateNodeForHost(node);
    380 
    381         // Add the virtual descendants.
    382         if (mTempArray == null) {
    383             mTempArray = new IntArray();
    384         } else {
    385             mTempArray.clear();
    386         }
    387         final IntArray virtualViewIds = mTempArray;
    388         getVisibleVirtualViews(virtualViewIds);
    389         if (realNodeCount > 0 && virtualViewIds.size() > 0) {
    390             throw new RuntimeException("Views cannot have both real and virtual children");
    391         }
    392 
    393         final int N = virtualViewIds.size();
    394         for (int i = 0; i < N; i++) {
    395             node.addChild(mView, virtualViewIds.get(i));
    396         }
    397 
    398         return node;
    399     }
    400 
    401     /**
    402      * Constructs and returns an {@link AccessibilityNodeInfo} for the
    403      * specified item. Automatically manages accessibility focus actions.
    404      * <p>
    405      * Allows the implementing class to specify most node properties, but
    406      * overrides the following:
    407      * <ul>
    408      * <li>{@link AccessibilityNodeInfo#setPackageName}
    409      * <li>{@link AccessibilityNodeInfo#setClassName}
    410      * <li>{@link AccessibilityNodeInfo#setParent(View)}
    411      * <li>{@link AccessibilityNodeInfo#setSource(View, int)}
    412      * <li>{@link AccessibilityNodeInfo#setVisibleToUser}
    413      * <li>{@link AccessibilityNodeInfo#setBoundsInScreen(Rect)}
    414      * </ul>
    415      * <p>
    416      * Uses the bounds of the parent view and the parent-relative bounding
    417      * rectangle specified by
    418      * {@link AccessibilityNodeInfo#getBoundsInParent} to automatically
    419      * update the following properties:
    420      * <ul>
    421      * <li>{@link AccessibilityNodeInfo#setVisibleToUser}
    422      * <li>{@link AccessibilityNodeInfo#setBoundsInParent}
    423      * </ul>
    424      *
    425      * @param virtualViewId The virtual view id for item for which to construct
    426      *            a node.
    427      * @return An {@link AccessibilityNodeInfo} for the specified item.
    428      */
    429     private AccessibilityNodeInfo createNodeForChild(int virtualViewId) {
    430         ensureTempRects();
    431         final Rect tempParentRect = mTempParentRect;
    432         final int[] tempGlobalRect = mTempGlobalRect;
    433         final Rect tempScreenRect = mTempScreenRect;
    434 
    435         final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
    436 
    437         // Ensure the client has good defaults.
    438         node.setEnabled(true);
    439         node.setClassName(DEFAULT_CLASS_NAME);
    440         node.setBoundsInParent(INVALID_PARENT_BOUNDS);
    441 
    442         // Allow the client to populate the node.
    443         onPopulateNodeForVirtualView(virtualViewId, node);
    444 
    445         // Make sure the developer is following the rules.
    446         if ((node.getText() == null) && (node.getContentDescription() == null)) {
    447             throw new RuntimeException("Callbacks must add text or a content description in "
    448                     + "populateNodeForVirtualViewId()");
    449         }
    450 
    451         node.getBoundsInParent(tempParentRect);
    452         if (tempParentRect.equals(INVALID_PARENT_BOUNDS)) {
    453             throw new RuntimeException("Callbacks must set parent bounds in "
    454                     + "populateNodeForVirtualViewId()");
    455         }
    456 
    457         final int actions = node.getActions();
    458         if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) {
    459             throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in "
    460                     + "populateNodeForVirtualViewId()");
    461         }
    462         if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) {
    463             throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in "
    464                     + "populateNodeForVirtualViewId()");
    465         }
    466 
    467         // Don't allow the client to override these properties.
    468         node.setPackageName(mView.getContext().getPackageName());
    469         node.setSource(mView, virtualViewId);
    470         node.setParent(mView);
    471 
    472         // Manage internal accessibility focus state.
    473         if (mFocusedVirtualViewId == virtualViewId) {
    474             node.setAccessibilityFocused(true);
    475             node.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
    476         } else {
    477             node.setAccessibilityFocused(false);
    478             node.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
    479         }
    480 
    481         // Set the visibility based on the parent bound.
    482         if (intersectVisibleToUser(tempParentRect)) {
    483             node.setVisibleToUser(true);
    484             node.setBoundsInParent(tempParentRect);
    485         }
    486 
    487         // Calculate screen-relative bound.
    488         mView.getLocationOnScreen(tempGlobalRect);
    489         final int offsetX = tempGlobalRect[0];
    490         final int offsetY = tempGlobalRect[1];
    491         tempScreenRect.set(tempParentRect);
    492         tempScreenRect.offset(offsetX, offsetY);
    493         node.setBoundsInScreen(tempScreenRect);
    494 
    495         return node;
    496     }
    497 
    498     private void ensureTempRects() {
    499         mTempGlobalRect = new int[2];
    500         mTempParentRect = new Rect();
    501         mTempScreenRect = new Rect();
    502     }
    503 
    504     private boolean performAction(int virtualViewId, int action, Bundle arguments) {
    505         switch (virtualViewId) {
    506             case HOST_ID:
    507                 return performActionForHost(action, arguments);
    508             default:
    509                 return performActionForChild(virtualViewId, action, arguments);
    510         }
    511     }
    512 
    513     private boolean performActionForHost(int action, Bundle arguments) {
    514         return mView.performAccessibilityAction(action, arguments);
    515     }
    516 
    517     private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) {
    518         switch (action) {
    519             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
    520             case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
    521                 return manageFocusForChild(virtualViewId, action);
    522             default:
    523                 return onPerformActionForVirtualView(virtualViewId, action, arguments);
    524         }
    525     }
    526 
    527     private boolean manageFocusForChild(int virtualViewId, int action) {
    528         switch (action) {
    529             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
    530                 return requestAccessibilityFocus(virtualViewId);
    531             case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
    532                 return clearAccessibilityFocus(virtualViewId);
    533             default:
    534                 return false;
    535         }
    536     }
    537 
    538     /**
    539      * Computes whether the specified {@link Rect} intersects with the visible
    540      * portion of its parent {@link View}. Modifies {@code localRect} to contain
    541      * only the visible portion.
    542      *
    543      * @param localRect A rectangle in local (parent) coordinates.
    544      * @return Whether the specified {@link Rect} is visible on the screen.
    545      */
    546     private boolean intersectVisibleToUser(Rect localRect) {
    547         // Missing or empty bounds mean this view is not visible.
    548         if ((localRect == null) || localRect.isEmpty()) {
    549             return false;
    550         }
    551 
    552         // Attached to invisible window means this view is not visible.
    553         if (mView.getWindowVisibility() != View.VISIBLE) {
    554             return false;
    555         }
    556 
    557         // An invisible predecessor means that this view is not visible.
    558         ViewParent viewParent = mView.getParent();
    559         while (viewParent instanceof View) {
    560             final View view = (View) viewParent;
    561             if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) {
    562                 return false;
    563             }
    564             viewParent = view.getParent();
    565         }
    566 
    567         // A null parent implies the view is not visible.
    568         if (viewParent == null) {
    569             return false;
    570         }
    571 
    572         // If no portion of the parent is visible, this view is not visible.
    573         if (mTempVisibleRect == null) {
    574             mTempVisibleRect = new Rect();
    575         }
    576         final Rect tempVisibleRect = mTempVisibleRect;
    577         if (!mView.getLocalVisibleRect(tempVisibleRect)) {
    578             return false;
    579         }
    580 
    581         // Check if the view intersects the visible portion of the parent.
    582         return localRect.intersect(tempVisibleRect);
    583     }
    584 
    585     /**
    586      * Returns whether this virtual view is accessibility focused.
    587      *
    588      * @return True if the view is accessibility focused.
    589      */
    590     private boolean isAccessibilityFocused(int virtualViewId) {
    591         return (mFocusedVirtualViewId == virtualViewId);
    592     }
    593 
    594     /**
    595      * Attempts to give accessibility focus to a virtual view.
    596      * <p>
    597      * A virtual view will not actually take focus if
    598      * {@link AccessibilityManager#isEnabled()} returns false,
    599      * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false,
    600      * or the view already has accessibility focus.
    601      *
    602      * @param virtualViewId The id of the virtual view on which to place
    603      *            accessibility focus.
    604      * @return Whether this virtual view actually took accessibility focus.
    605      */
    606     private boolean requestAccessibilityFocus(int virtualViewId) {
    607         final AccessibilityManager accessibilityManager =
    608                 (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
    609 
    610         if (!mManager.isEnabled()
    611                 || !accessibilityManager.isTouchExplorationEnabled()) {
    612             return false;
    613         }
    614         // TODO: Check virtual view visibility.
    615         if (!isAccessibilityFocused(virtualViewId)) {
    616             // Clear focus from the previously focused view, if applicable.
    617             if (mFocusedVirtualViewId != INVALID_ID) {
    618                 sendEventForVirtualView(mFocusedVirtualViewId,
    619                         AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
    620             }
    621 
    622             // Set focus on the new view.
    623             mFocusedVirtualViewId = virtualViewId;
    624 
    625             // TODO: Only invalidate virtual view bounds.
    626             mView.invalidate();
    627             sendEventForVirtualView(virtualViewId,
    628                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
    629             return true;
    630         }
    631         return false;
    632     }
    633 
    634     /**
    635      * Attempts to clear accessibility focus from a virtual view.
    636      *
    637      * @param virtualViewId The id of the virtual view from which to clear
    638      *            accessibility focus.
    639      * @return Whether this virtual view actually cleared accessibility focus.
    640      */
    641     private boolean clearAccessibilityFocus(int virtualViewId) {
    642         if (isAccessibilityFocused(virtualViewId)) {
    643             mFocusedVirtualViewId = INVALID_ID;
    644             mView.invalidate();
    645             sendEventForVirtualView(virtualViewId,
    646                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
    647             return true;
    648         }
    649         return false;
    650     }
    651 
    652     /**
    653      * Provides a mapping between view-relative coordinates and logical
    654      * items.
    655      *
    656      * @param x The view-relative x coordinate
    657      * @param y The view-relative y coordinate
    658      * @return virtual view identifier for the logical item under
    659      *         coordinates (x,y)
    660      */
    661     protected abstract int getVirtualViewAt(float x, float y);
    662 
    663     /**
    664      * Populates a list with the view's visible items. The ordering of items
    665      * within {@code virtualViewIds} specifies order of accessibility focus
    666      * traversal.
    667      *
    668      * @param virtualViewIds The list to populate with visible items
    669      */
    670     protected abstract void getVisibleVirtualViews(IntArray virtualViewIds);
    671 
    672     /**
    673      * Populates an {@link AccessibilityEvent} with information about the
    674      * specified item.
    675      * <p>
    676      * Implementations <b>must</b> populate the following required fields:
    677      * <ul>
    678      * <li>event text, see {@link AccessibilityEvent#getText} or
    679      * {@link AccessibilityEvent#setContentDescription}
    680      * </ul>
    681      * <p>
    682      * The helper class automatically populates the following fields with
    683      * default values, but implementations may optionally override them:
    684      * <ul>
    685      * <li>item class name, set to android.view.View, see
    686      * {@link AccessibilityEvent#setClassName}
    687      * </ul>
    688      * <p>
    689      * The following required fields are automatically populated by the
    690      * helper class and may not be overridden:
    691      * <ul>
    692      * <li>package name, set to the package of the host view's
    693      * {@link Context}, see {@link AccessibilityEvent#setPackageName}
    694      * <li>event source, set to the host view and virtual view identifier,
    695      * see {@link android.view.accessibility.AccessibilityRecord#setSource(View, int)}
    696      * </ul>
    697      *
    698      * @param virtualViewId The virtual view id for the item for which to
    699      *            populate the event
    700      * @param event The event to populate
    701      */
    702     protected abstract void onPopulateEventForVirtualView(
    703             int virtualViewId, AccessibilityEvent event);
    704 
    705     /**
    706      * Populates an {@link AccessibilityEvent} with information about the host
    707      * view.
    708      * <p>
    709      * The default implementation is a no-op.
    710      *
    711      * @param event the event to populate with information about the host view
    712      */
    713     protected void onPopulateEventForHost(AccessibilityEvent event) {
    714         // Default implementation is no-op.
    715     }
    716 
    717     /**
    718      * Populates an {@link AccessibilityNodeInfo} with information
    719      * about the specified item.
    720      * <p>
    721      * Implementations <b>must</b> populate the following required fields:
    722      * <ul>
    723      * <li>event text, see {@link AccessibilityNodeInfo#setText} or
    724      * {@link AccessibilityNodeInfo#setContentDescription}
    725      * <li>bounds in parent coordinates, see
    726      * {@link AccessibilityNodeInfo#setBoundsInParent}
    727      * </ul>
    728      * <p>
    729      * The helper class automatically populates the following fields with
    730      * default values, but implementations may optionally override them:
    731      * <ul>
    732      * <li>enabled state, set to true, see
    733      * {@link AccessibilityNodeInfo#setEnabled}
    734      * <li>item class name, identical to the class name set by
    735      * {@link #onPopulateEventForVirtualView}, see
    736      * {@link AccessibilityNodeInfo#setClassName}
    737      * </ul>
    738      * <p>
    739      * The following required fields are automatically populated by the
    740      * helper class and may not be overridden:
    741      * <ul>
    742      * <li>package name, identical to the package name set by
    743      * {@link #onPopulateEventForVirtualView}, see
    744      * {@link AccessibilityNodeInfo#setPackageName}
    745      * <li>node source, identical to the event source set in
    746      * {@link #onPopulateEventForVirtualView}, see
    747      * {@link AccessibilityNodeInfo#setSource(View, int)}
    748      * <li>parent view, set to the host view, see
    749      * {@link AccessibilityNodeInfo#setParent(View)}
    750      * <li>visibility, computed based on parent-relative bounds, see
    751      * {@link AccessibilityNodeInfo#setVisibleToUser}
    752      * <li>accessibility focus, computed based on internal helper state, see
    753      * {@link AccessibilityNodeInfo#setAccessibilityFocused}
    754      * <li>bounds in screen coordinates, computed based on host view bounds,
    755      * see {@link AccessibilityNodeInfo#setBoundsInScreen}
    756      * </ul>
    757      * <p>
    758      * Additionally, the helper class automatically handles accessibility
    759      * focus management by adding the appropriate
    760      * {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} or
    761      * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
    762      * action. Implementations must <b>never</b> manually add these actions.
    763      * <p>
    764      * The helper class also automatically modifies parent- and
    765      * screen-relative bounds to reflect the portion of the item visible
    766      * within its parent.
    767      *
    768      * @param virtualViewId The virtual view identifier of the item for
    769      *            which to populate the node
    770      * @param node The node to populate
    771      */
    772     protected abstract void onPopulateNodeForVirtualView(
    773             int virtualViewId, AccessibilityNodeInfo node);
    774 
    775     /**
    776      * Populates an {@link AccessibilityNodeInfo} with information about the
    777      * host view.
    778      * <p>
    779      * The default implementation is a no-op.
    780      *
    781      * @param node the node to populate with information about the host view
    782      */
    783     protected void onPopulateNodeForHost(AccessibilityNodeInfo node) {
    784         // Default implementation is no-op.
    785     }
    786 
    787     /**
    788      * Performs the specified accessibility action on the item associated
    789      * with the virtual view identifier. See
    790      * {@link AccessibilityNodeInfo#performAction(int, Bundle)} for
    791      * more information.
    792      * <p>
    793      * Implementations <b>must</b> handle any actions added manually in
    794      * {@link #onPopulateNodeForVirtualView}.
    795      * <p>
    796      * The helper class automatically handles focus management resulting
    797      * from {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS}
    798      * and
    799      * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
    800      * actions.
    801      *
    802      * @param virtualViewId The virtual view identifier of the item on which
    803      *            to perform the action
    804      * @param action The accessibility action to perform
    805      * @param arguments (Optional) A bundle with additional arguments, or
    806      *            null
    807      * @return true if the action was performed
    808      */
    809     protected abstract boolean onPerformActionForVirtualView(
    810             int virtualViewId, int action, Bundle arguments);
    811 
    812     /**
    813      * Exposes a virtual view hierarchy to the accessibility framework. Only
    814      * used in API 16+.
    815      */
    816     private class ExploreByTouchNodeProvider extends AccessibilityNodeProvider {
    817         @Override
    818         public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
    819             return ExploreByTouchHelper.this.createNode(virtualViewId);
    820         }
    821 
    822         @Override
    823         public boolean performAction(int virtualViewId, int action, Bundle arguments) {
    824             return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments);
    825         }
    826     }
    827 }
    828