Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright 2018 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 androidx.customview.widget;
     18 
     19 import android.content.Context;
     20 import android.graphics.Rect;
     21 import android.os.Bundle;
     22 import android.view.KeyEvent;
     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.AccessibilityRecord;
     29 
     30 import androidx.annotation.NonNull;
     31 import androidx.annotation.Nullable;
     32 import androidx.collection.SparseArrayCompat;
     33 import androidx.core.view.AccessibilityDelegateCompat;
     34 import androidx.core.view.ViewCompat;
     35 import androidx.core.view.ViewCompat.FocusDirection;
     36 import androidx.core.view.ViewCompat.FocusRealDirection;
     37 import androidx.core.view.ViewParentCompat;
     38 import androidx.core.view.accessibility.AccessibilityEventCompat;
     39 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
     40 import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
     41 import androidx.core.view.accessibility.AccessibilityRecordCompat;
     42 
     43 import java.util.ArrayList;
     44 import java.util.List;
     45 
     46 /**
     47  * ExploreByTouchHelper is a utility class for implementing accessibility
     48  * support in custom {@link View}s that represent a collection of View-like
     49  * logical items. It extends {@link AccessibilityNodeProviderCompat} and
     50  * simplifies many aspects of providing information to accessibility services
     51  * and managing accessibility focus.
     52  * <p>
     53  * Clients should override abstract methods on this class and attach it to the
     54  * host view using {@link ViewCompat#setAccessibilityDelegate}:
     55  * <p>
     56  * <pre>
     57  * class MyCustomView extends View {
     58  *     private MyVirtualViewHelper mVirtualViewHelper;
     59  *
     60  *     public MyCustomView(Context context, ...) {
     61  *         ...
     62  *         mVirtualViewHelper = new MyVirtualViewHelper(this);
     63  *         ViewCompat.setAccessibilityDelegate(this, mVirtualViewHelper);
     64  *     }
     65  *
     66  *     &#64;Override
     67  *     public boolean dispatchHoverEvent(MotionEvent event) {
     68  *       return mHelper.dispatchHoverEvent(this, event)
     69  *           || super.dispatchHoverEvent(event);
     70  *     }
     71  *
     72  *     &#64;Override
     73  *     public boolean dispatchKeyEvent(KeyEvent event) {
     74  *       return mHelper.dispatchKeyEvent(event)
     75  *           || super.dispatchKeyEvent(event);
     76  *     }
     77  *
     78  *     &#64;Override
     79  *     public boolean onFocusChanged(boolean gainFocus, int direction,
     80  *         Rect previouslyFocusedRect) {
     81  *       super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
     82  *       mHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
     83  *     }
     84  * }
     85  * mAccessHelper = new MyExploreByTouchHelper(someView);
     86  * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper);
     87  * </pre>
     88  */
     89 public abstract class ExploreByTouchHelper extends AccessibilityDelegateCompat {
     90     /** Virtual node identifier value for invalid nodes. */
     91     public static final int INVALID_ID = Integer.MIN_VALUE;
     92 
     93     /** Virtual node identifier value for the host view's node. */
     94     public static final int HOST_ID = View.NO_ID;
     95 
     96     /** Default class name used for virtual views. */
     97     private static final String DEFAULT_CLASS_NAME = "android.view.View";
     98 
     99     /** Default bounds used to determine if the client didn't set any. */
    100     private static final Rect INVALID_PARENT_BOUNDS = new Rect(
    101             Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
    102 
    103     // Temporary, reusable data structures.
    104     private final Rect mTempScreenRect = new Rect();
    105     private final Rect mTempParentRect = new Rect();
    106     private final Rect mTempVisibleRect = new Rect();
    107     private final int[] mTempGlobalRect = new int[2];
    108 
    109     /** System accessibility manager, used to check state and send events. */
    110     private final AccessibilityManager mManager;
    111 
    112     /** View whose internal structure is exposed through this helper. */
    113     private final View mHost;
    114 
    115     /** Virtual node provider used to expose logical structure to services. */
    116     private MyNodeProvider mNodeProvider;
    117 
    118     /** Identifier for the virtual view that holds accessibility focus. */
    119     private int mAccessibilityFocusedVirtualViewId = INVALID_ID;
    120 
    121     /** Identifier for the virtual view that holds keyboard focus. */
    122     private int mKeyboardFocusedVirtualViewId = INVALID_ID;
    123 
    124     /** Identifier for the virtual view that is currently hovered. */
    125     private int mHoveredVirtualViewId = INVALID_ID;
    126 
    127     /**
    128      * Constructs a new helper that can expose a virtual view hierarchy for the
    129      * specified host view.
    130      *
    131      * @param host view whose virtual view hierarchy is exposed by this helper
    132      */
    133     public ExploreByTouchHelper(@NonNull View host) {
    134         if (host == null) {
    135             throw new IllegalArgumentException("View may not be null");
    136         }
    137 
    138         mHost = host;
    139 
    140         final Context context = host.getContext();
    141         mManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
    142 
    143         // Host view must be focusable so that we can delegate to virtual
    144         // views.
    145         host.setFocusable(true);
    146         if (ViewCompat.getImportantForAccessibility(host)
    147                 == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
    148             ViewCompat.setImportantForAccessibility(
    149                     host, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
    150         }
    151     }
    152 
    153     @Override
    154     public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
    155         if (mNodeProvider == null) {
    156             mNodeProvider = new MyNodeProvider();
    157         }
    158         return mNodeProvider;
    159     }
    160 
    161     /**
    162      * Delegates hover events from the host view.
    163      * <p>
    164      * Dispatches hover {@link MotionEvent}s to the virtual view hierarchy when
    165      * the Explore by Touch feature is enabled.
    166      * <p>
    167      * This method should be called by overriding the host view's
    168      * {@link View#dispatchHoverEvent(MotionEvent)} method:
    169      * <pre>&#64;Override
    170      * public boolean dispatchHoverEvent(MotionEvent event) {
    171      *   return mHelper.dispatchHoverEvent(this, event)
    172      *       || super.dispatchHoverEvent(event);
    173      * }
    174      * </pre>
    175      *
    176      * @param event The hover event to dispatch to the virtual view hierarchy.
    177      * @return Whether the hover event was handled.
    178      */
    179     public final boolean dispatchHoverEvent(@NonNull MotionEvent event) {
    180         if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) {
    181             return false;
    182         }
    183 
    184         switch (event.getAction()) {
    185             case MotionEvent.ACTION_HOVER_MOVE:
    186             case MotionEvent.ACTION_HOVER_ENTER:
    187                 final int virtualViewId = getVirtualViewAt(event.getX(), event.getY());
    188                 updateHoveredVirtualView(virtualViewId);
    189                 return (virtualViewId != INVALID_ID);
    190             case MotionEvent.ACTION_HOVER_EXIT:
    191                 if (mHoveredVirtualViewId != INVALID_ID) {
    192                     updateHoveredVirtualView(INVALID_ID);
    193                     return true;
    194                 }
    195                 return false;
    196             default:
    197                 return false;
    198         }
    199     }
    200 
    201     /**
    202      * Delegates key events from the host view.
    203      * <p>
    204      * This method should be called by overriding the host view's
    205      * {@link View#dispatchKeyEvent(KeyEvent)} method:
    206      * <pre>&#64;Override
    207      * public boolean dispatchKeyEvent(KeyEvent event) {
    208      *   return mHelper.dispatchKeyEvent(event)
    209      *       || super.dispatchKeyEvent(event);
    210      * }
    211      * </pre>
    212      */
    213     public final boolean dispatchKeyEvent(@NonNull KeyEvent event) {
    214         boolean handled = false;
    215 
    216         final int action = event.getAction();
    217         if (action != KeyEvent.ACTION_UP) {
    218             final int keyCode = event.getKeyCode();
    219             switch (keyCode) {
    220                 case KeyEvent.KEYCODE_DPAD_LEFT:
    221                 case KeyEvent.KEYCODE_DPAD_UP:
    222                 case KeyEvent.KEYCODE_DPAD_RIGHT:
    223                 case KeyEvent.KEYCODE_DPAD_DOWN:
    224                     if (event.hasNoModifiers()) {
    225                         final int direction = keyToDirection(keyCode);
    226                         final int count = 1 + event.getRepeatCount();
    227                         for (int i = 0; i < count; i++) {
    228                             if (moveFocus(direction, null)) {
    229                                 handled = true;
    230                             } else {
    231                                 break;
    232                             }
    233                         }
    234                     }
    235                     break;
    236                 case KeyEvent.KEYCODE_DPAD_CENTER:
    237                 case KeyEvent.KEYCODE_ENTER:
    238                     if (event.hasNoModifiers()) {
    239                         if (event.getRepeatCount() == 0) {
    240                             clickKeyboardFocusedVirtualView();
    241                             handled = true;
    242                         }
    243                     }
    244                     break;
    245                 case KeyEvent.KEYCODE_TAB:
    246                     if (event.hasNoModifiers()) {
    247                         handled = moveFocus(View.FOCUS_FORWARD, null);
    248                     } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
    249                         handled = moveFocus(View.FOCUS_BACKWARD, null);
    250                     }
    251                     break;
    252             }
    253         }
    254 
    255         return handled;
    256     }
    257 
    258     /**
    259      * Delegates focus changes from the host view.
    260      * <p>
    261      * This method should be called by overriding the host view's
    262      * {@link View#onFocusChanged(boolean, int, Rect)} method:
    263      * <pre>&#64;Override
    264      * public boolean onFocusChanged(boolean gainFocus, int direction,
    265      *     Rect previouslyFocusedRect) {
    266      *   super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    267      *   mHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    268      * }
    269      * </pre>
    270      */
    271     public final void onFocusChanged(boolean gainFocus, int direction,
    272             @Nullable Rect previouslyFocusedRect) {
    273         if (mKeyboardFocusedVirtualViewId != INVALID_ID) {
    274             clearKeyboardFocusForVirtualView(mKeyboardFocusedVirtualViewId);
    275         }
    276 
    277         if (gainFocus) {
    278             moveFocus(direction, previouslyFocusedRect);
    279         }
    280     }
    281 
    282     /**
    283      * @return the identifier of the virtual view that has accessibility focus
    284      *         or {@link #INVALID_ID} if no virtual view has accessibility
    285      *         focus
    286      */
    287     public final int getAccessibilityFocusedVirtualViewId() {
    288         return mAccessibilityFocusedVirtualViewId;
    289     }
    290 
    291     /**
    292      * @return the identifier of the virtual view that has keyboard focus
    293      *         or {@link #INVALID_ID} if no virtual view has keyboard focus
    294      */
    295     public final int getKeyboardFocusedVirtualViewId() {
    296         return mKeyboardFocusedVirtualViewId;
    297     }
    298 
    299     /**
    300      * Maps key event codes to focus directions.
    301      *
    302      * @param keyCode the key event code
    303      * @return the corresponding focus direction
    304      */
    305     @FocusRealDirection
    306     private static int keyToDirection(int keyCode) {
    307         switch (keyCode) {
    308             case KeyEvent.KEYCODE_DPAD_LEFT:
    309                 return View.FOCUS_LEFT;
    310             case KeyEvent.KEYCODE_DPAD_UP:
    311                 return View.FOCUS_UP;
    312             case KeyEvent.KEYCODE_DPAD_RIGHT:
    313                 return View.FOCUS_RIGHT;
    314             default:
    315                 return View.FOCUS_DOWN;
    316         }
    317     }
    318 
    319     /**
    320      * Obtains the bounds for the specified virtual view.
    321      *
    322      * @param virtualViewId the identifier of the virtual view
    323      * @param outBounds the rect to populate with virtual view bounds
    324      */
    325     private void getBoundsInParent(int virtualViewId, Rect outBounds) {
    326         final AccessibilityNodeInfoCompat node = obtainAccessibilityNodeInfo(virtualViewId);
    327         node.getBoundsInParent(outBounds);
    328     }
    329 
    330     /**
    331      * Adapts AccessibilityNodeInfoCompat for obtaining bounds.
    332      */
    333     private static final FocusStrategy.BoundsAdapter<AccessibilityNodeInfoCompat> NODE_ADAPTER =
    334             new FocusStrategy.BoundsAdapter<AccessibilityNodeInfoCompat>() {
    335                 @Override
    336                 public void obtainBounds(AccessibilityNodeInfoCompat node, Rect outBounds) {
    337                     node.getBoundsInParent(outBounds);
    338                 }
    339             };
    340 
    341     /**
    342      * Adapts SparseArrayCompat for iterating through values.
    343      */
    344     private static final FocusStrategy.CollectionAdapter<SparseArrayCompat<
    345             AccessibilityNodeInfoCompat>, AccessibilityNodeInfoCompat> SPARSE_VALUES_ADAPTER =
    346             new FocusStrategy.CollectionAdapter<SparseArrayCompat<
    347                     AccessibilityNodeInfoCompat>, AccessibilityNodeInfoCompat>() {
    348                 @Override
    349                 public AccessibilityNodeInfoCompat get(
    350                         SparseArrayCompat<AccessibilityNodeInfoCompat> collection, int index) {
    351                     return collection.valueAt(index);
    352                 }
    353 
    354                 @Override
    355                 public int size(SparseArrayCompat<AccessibilityNodeInfoCompat> collection) {
    356                     return collection.size();
    357                 }
    358             };
    359 
    360     /**
    361      * Attempts to move keyboard focus in the specified direction.
    362      *
    363      * @param direction the direction in which to move keyboard focus
    364      * @param previouslyFocusedRect the bounds of the previously focused item,
    365      *                              or {@code null} if not available
    366      * @return {@code true} if keyboard focus moved to a virtual view managed
    367      *         by this helper, or {@code false} otherwise
    368      */
    369     private boolean moveFocus(@FocusDirection int direction, @Nullable Rect previouslyFocusedRect) {
    370         final SparseArrayCompat<AccessibilityNodeInfoCompat> allNodes = getAllNodes();
    371 
    372         final int focusedNodeId = mKeyboardFocusedVirtualViewId;
    373         final AccessibilityNodeInfoCompat focusedNode =
    374                 focusedNodeId == INVALID_ID ? null : allNodes.get(focusedNodeId);
    375 
    376         final AccessibilityNodeInfoCompat nextFocusedNode;
    377         switch (direction) {
    378             case View.FOCUS_FORWARD:
    379             case View.FOCUS_BACKWARD:
    380                 final boolean isLayoutRtl =
    381                         ViewCompat.getLayoutDirection(mHost) == ViewCompat.LAYOUT_DIRECTION_RTL;
    382                 nextFocusedNode = FocusStrategy.findNextFocusInRelativeDirection(allNodes,
    383                         SPARSE_VALUES_ADAPTER, NODE_ADAPTER, focusedNode, direction, isLayoutRtl,
    384                         false);
    385                 break;
    386             case View.FOCUS_LEFT:
    387             case View.FOCUS_UP:
    388             case View.FOCUS_RIGHT:
    389             case View.FOCUS_DOWN:
    390                 final Rect selectedRect = new Rect();
    391                 if (mKeyboardFocusedVirtualViewId != INVALID_ID) {
    392                     // Focus is moving from a virtual view within the host.
    393                     getBoundsInParent(mKeyboardFocusedVirtualViewId, selectedRect);
    394                 } else if (previouslyFocusedRect != null) {
    395                     // Focus is moving from a real view outside the host.
    396                     selectedRect.set(previouslyFocusedRect);
    397                 } else {
    398                     // Focus is moving from... somewhere? Make a guess.
    399                     // Usually this happens when another view was too lazy
    400                     // to pass the previously focused rect (ex. ScrollView
    401                     // when moving UP or DOWN).
    402                     guessPreviouslyFocusedRect(mHost, direction, selectedRect);
    403                 }
    404                 nextFocusedNode = FocusStrategy.findNextFocusInAbsoluteDirection(allNodes,
    405                         SPARSE_VALUES_ADAPTER, NODE_ADAPTER, focusedNode, selectedRect, direction);
    406                 break;
    407             default:
    408                 throw new IllegalArgumentException("direction must be one of "
    409                         + "{FOCUS_FORWARD, FOCUS_BACKWARD, FOCUS_UP, FOCUS_DOWN, "
    410                         + "FOCUS_LEFT, FOCUS_RIGHT}.");
    411         }
    412 
    413         final int nextFocusedNodeId;
    414         if (nextFocusedNode == null) {
    415             nextFocusedNodeId = INVALID_ID;
    416         } else {
    417             final int index = allNodes.indexOfValue(nextFocusedNode);
    418             nextFocusedNodeId = allNodes.keyAt(index);
    419         }
    420 
    421         return requestKeyboardFocusForVirtualView(nextFocusedNodeId);
    422     }
    423 
    424     private SparseArrayCompat<AccessibilityNodeInfoCompat> getAllNodes() {
    425         final List<Integer> virtualViewIds = new ArrayList<>();
    426         getVisibleVirtualViews(virtualViewIds);
    427 
    428         final SparseArrayCompat<AccessibilityNodeInfoCompat> allNodes = new SparseArrayCompat<>();
    429         for (int virtualViewId = 0; virtualViewId < virtualViewIds.size(); virtualViewId++) {
    430             final AccessibilityNodeInfoCompat virtualView = createNodeForChild(virtualViewId);
    431             allNodes.put(virtualViewId, virtualView);
    432         }
    433 
    434         return allNodes;
    435     }
    436 
    437     /**
    438      * Obtains a best guess for the previously focused rect for keyboard focus
    439      * moving in the specified direction.
    440      *
    441      * @param host the view into which focus is moving
    442      * @param direction the absolute direction in which focus is moving
    443      * @param outBounds the rect to populate with the best-guess bounds for the
    444      *                  previous focus rect
    445      */
    446     private static Rect guessPreviouslyFocusedRect(@NonNull View host,
    447             @FocusRealDirection int direction, @NonNull Rect outBounds) {
    448         final int w = host.getWidth();
    449         final int h = host.getHeight();
    450 
    451         switch (direction) {
    452             case View.FOCUS_LEFT:
    453                 outBounds.set(w, 0, w, h);
    454                 break;
    455             case View.FOCUS_UP:
    456                 outBounds.set(0, h, w, h);
    457                 break;
    458             case View.FOCUS_RIGHT:
    459                 outBounds.set(-1, 0, -1, h);
    460                 break;
    461             case View.FOCUS_DOWN:
    462                 outBounds.set(0, -1, w, -1);
    463                 break;
    464             default:
    465                 throw new IllegalArgumentException("direction must be one of "
    466                         + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    467         }
    468 
    469         return outBounds;
    470     }
    471 
    472     /**
    473      * Performs a click action on the keyboard focused virtual view, if any.
    474      *
    475      * @return {@code true} if the click action was performed successfully or
    476      *         {@code false} otherwise
    477      */
    478     private boolean clickKeyboardFocusedVirtualView() {
    479         return mKeyboardFocusedVirtualViewId != INVALID_ID && onPerformActionForVirtualView(
    480                 mKeyboardFocusedVirtualViewId, AccessibilityNodeInfoCompat.ACTION_CLICK, null);
    481     }
    482 
    483     /**
    484      * Populates an event of the specified type with information about an item
    485      * and attempts to send it up through the view hierarchy.
    486      * <p>
    487      * You should call this method after performing a user action that normally
    488      * fires an accessibility event, such as clicking on an item.
    489      * <p>
    490      * <pre>public void performItemClick(T item) {
    491      *   ...
    492      *   sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED);
    493      * }
    494      * </pre>
    495      *
    496      * @param virtualViewId the identifier of the virtual view for which to
    497      *                      send an event
    498      * @param eventType the type of event to send
    499      * @return {@code true} if the event was sent successfully, {@code false}
    500      *         otherwise
    501      */
    502     public final boolean sendEventForVirtualView(int virtualViewId, int eventType) {
    503         if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) {
    504             return false;
    505         }
    506 
    507         final ViewParent parent = mHost.getParent();
    508         if (parent == null) {
    509             return false;
    510         }
    511 
    512         final AccessibilityEvent event = createEvent(virtualViewId, eventType);
    513         return ViewParentCompat.requestSendAccessibilityEvent(parent, mHost, event);
    514     }
    515 
    516     /**
    517      * Notifies the accessibility framework that the properties of the parent
    518      * view have changed.
    519      * <p>
    520      * You <strong>must</strong> call this method after adding or removing
    521      * items from the parent view.
    522      */
    523     public final void invalidateRoot() {
    524         invalidateVirtualView(HOST_ID, AccessibilityEventCompat.CONTENT_CHANGE_TYPE_SUBTREE);
    525     }
    526 
    527     /**
    528      * Notifies the accessibility framework that the properties of a particular
    529      * item have changed.
    530      * <p>
    531      * You <strong>must</strong> call this method after changing any of the
    532      * properties set in
    533      * {@link #onPopulateNodeForVirtualView(int, AccessibilityNodeInfoCompat)}.
    534      *
    535      * @param virtualViewId the virtual view id to invalidate, or
    536      *                      {@link #HOST_ID} to invalidate the root view
    537      * @see #invalidateVirtualView(int, int)
    538      */
    539     public final void invalidateVirtualView(int virtualViewId) {
    540         invalidateVirtualView(virtualViewId,
    541                 AccessibilityEventCompat.CONTENT_CHANGE_TYPE_UNDEFINED);
    542     }
    543 
    544     /**
    545      * Notifies the accessibility framework that the properties of a particular
    546      * item have changed.
    547      * <p>
    548      * You <strong>must</strong> call this method after changing any of the
    549      * properties set in
    550      * {@link #onPopulateNodeForVirtualView(int, AccessibilityNodeInfoCompat)}.
    551      *
    552      * @param virtualViewId the virtual view id to invalidate, or
    553      *                      {@link #HOST_ID} to invalidate the root view
    554      * @param changeTypes the bit mask of change types. May be {@code 0} for the
    555      *                    default (undefined) change type or one or more of:
    556      *         <ul>
    557      *         <li>{@link AccessibilityEventCompat#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION}
    558      *         <li>{@link AccessibilityEventCompat#CONTENT_CHANGE_TYPE_SUBTREE}
    559      *         <li>{@link AccessibilityEventCompat#CONTENT_CHANGE_TYPE_TEXT}
    560      *         <li>{@link AccessibilityEventCompat#CONTENT_CHANGE_TYPE_UNDEFINED}
    561      *         </ul>
    562      */
    563     public final void invalidateVirtualView(int virtualViewId, int changeTypes) {
    564         if (virtualViewId != INVALID_ID && mManager.isEnabled()) {
    565             final ViewParent parent = mHost.getParent();
    566             if (parent != null) {
    567                 // Send events up the hierarchy so they can be coalesced.
    568                 final AccessibilityEvent event = createEvent(virtualViewId,
    569                         AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
    570                 AccessibilityEventCompat.setContentChangeTypes(event, changeTypes);
    571                 ViewParentCompat.requestSendAccessibilityEvent(parent, mHost, event);
    572             }
    573         }
    574     }
    575 
    576     /**
    577      * Returns the virtual view ID for the currently accessibility focused
    578      * item.
    579      *
    580      * @return the identifier of the virtual view that has accessibility focus
    581      *         or {@link #INVALID_ID} if no virtual view has accessibility
    582      *         focus
    583      * @deprecated Use {@link #getAccessibilityFocusedVirtualViewId()}.
    584      */
    585     @Deprecated
    586     public int getFocusedVirtualView() {
    587         return getAccessibilityFocusedVirtualViewId();
    588     }
    589 
    590     /**
    591      * Called when the focus state of a virtual view changes.
    592      *
    593      * @param virtualViewId the virtual view identifier
    594      * @param hasFocus      {@code true} if the view has focus, {@code false}
    595      *                      otherwise
    596      */
    597     protected void onVirtualViewKeyboardFocusChanged(int virtualViewId, boolean hasFocus) {
    598         // Stub method.
    599     }
    600 
    601     /**
    602      * Sets the currently hovered item, sending hover accessibility events as
    603      * necessary to maintain the correct state.
    604      *
    605      * @param virtualViewId the virtual view id for the item currently being
    606      *                      hovered, or {@link #INVALID_ID} if no item is
    607      *                      hovered within the parent view
    608      */
    609     private void updateHoveredVirtualView(int virtualViewId) {
    610         if (mHoveredVirtualViewId == virtualViewId) {
    611             return;
    612         }
    613 
    614         final int previousVirtualViewId = mHoveredVirtualViewId;
    615         mHoveredVirtualViewId = virtualViewId;
    616 
    617         // Stay consistent with framework behavior by sending ENTER/EXIT pairs
    618         // in reverse order. This is accurate as of API 18.
    619         sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
    620         sendEventForVirtualView(
    621                 previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
    622     }
    623 
    624     /**
    625      * Constructs and returns an {@link AccessibilityEvent} for the specified
    626      * virtual view id, which includes the host view ({@link #HOST_ID}).
    627      *
    628      * @param virtualViewId the virtual view id for the item for which to
    629      *                      construct an event
    630      * @param eventType the type of event to construct
    631      * @return an {@link AccessibilityEvent} populated with information about
    632      *         the specified item
    633      */
    634     private AccessibilityEvent createEvent(int virtualViewId, int eventType) {
    635         switch (virtualViewId) {
    636             case HOST_ID:
    637                 return createEventForHost(eventType);
    638             default:
    639                 return createEventForChild(virtualViewId, eventType);
    640         }
    641     }
    642 
    643     /**
    644      * Constructs and returns an {@link AccessibilityEvent} for the host node.
    645      *
    646      * @param eventType the type of event to construct
    647      * @return an {@link AccessibilityEvent} populated with information about
    648      *         the specified item
    649      */
    650     private AccessibilityEvent createEventForHost(int eventType) {
    651         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
    652         mHost.onInitializeAccessibilityEvent(event);
    653         return event;
    654     }
    655 
    656     @Override
    657     public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
    658         super.onInitializeAccessibilityEvent(host, event);
    659 
    660         // Allow the client to populate the event.
    661         onPopulateEventForHost(event);
    662     }
    663 
    664     /**
    665      * Constructs and returns an {@link AccessibilityEvent} populated with
    666      * information about the specified item.
    667      *
    668      * @param virtualViewId the virtual view id for the item for which to
    669      *                      construct an event
    670      * @param eventType the type of event to construct
    671      * @return an {@link AccessibilityEvent} populated with information about
    672      *         the specified item
    673      */
    674     private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) {
    675         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
    676         final AccessibilityNodeInfoCompat node = obtainAccessibilityNodeInfo(virtualViewId);
    677 
    678         // Allow the client to override these properties,
    679         event.getText().add(node.getText());
    680         event.setContentDescription(node.getContentDescription());
    681         event.setScrollable(node.isScrollable());
    682         event.setPassword(node.isPassword());
    683         event.setEnabled(node.isEnabled());
    684         event.setChecked(node.isChecked());
    685 
    686         // Allow the client to populate the event.
    687         onPopulateEventForVirtualView(virtualViewId, event);
    688 
    689         // Make sure the developer is following the rules.
    690         if (event.getText().isEmpty() && (event.getContentDescription() == null)) {
    691             throw new RuntimeException("Callbacks must add text or a content description in "
    692                     + "populateEventForVirtualViewId()");
    693         }
    694 
    695         // Don't allow the client to override these properties.
    696         event.setClassName(node.getClassName());
    697         AccessibilityRecordCompat.setSource(event, mHost, virtualViewId);
    698         event.setPackageName(mHost.getContext().getPackageName());
    699 
    700         return event;
    701     }
    702 
    703     /**
    704      * Obtains a populated {@link AccessibilityNodeInfoCompat} for the
    705      * virtual view with the specified identifier.
    706      * <p>
    707      * This method may be called with identifier {@link #HOST_ID} to obtain a
    708      * node for the host view.
    709      *
    710      * @param virtualViewId the identifier of the virtual view for which to
    711      *                      construct a node
    712      * @return an {@link AccessibilityNodeInfoCompat} populated with information
    713      *         about the specified item
    714      */
    715     @NonNull
    716     AccessibilityNodeInfoCompat obtainAccessibilityNodeInfo(int virtualViewId) {
    717         if (virtualViewId == HOST_ID) {
    718             return createNodeForHost();
    719         }
    720 
    721         return createNodeForChild(virtualViewId);
    722     }
    723 
    724     /**
    725      * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the
    726      * host view populated with its virtual descendants.
    727      *
    728      * @return an {@link AccessibilityNodeInfoCompat} for the parent node
    729      */
    730     @NonNull
    731     private AccessibilityNodeInfoCompat createNodeForHost() {
    732         final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(mHost);
    733         ViewCompat.onInitializeAccessibilityNodeInfo(mHost, info);
    734 
    735         // Add the virtual descendants.
    736         final ArrayList<Integer> virtualViewIds = new ArrayList<>();
    737         getVisibleVirtualViews(virtualViewIds);
    738 
    739         final int realNodeCount = info.getChildCount();
    740         if (realNodeCount > 0 && virtualViewIds.size() > 0) {
    741             throw new RuntimeException("Views cannot have both real and virtual children");
    742         }
    743 
    744         for (int i = 0, count = virtualViewIds.size(); i < count; i++) {
    745             info.addChild(mHost, virtualViewIds.get(i));
    746         }
    747 
    748         return info;
    749     }
    750 
    751     @Override
    752     public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
    753         super.onInitializeAccessibilityNodeInfo(host, info);
    754 
    755         // Allow the client to populate the host node.
    756         onPopulateNodeForHost(info);
    757     }
    758 
    759     /**
    760      * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the
    761      * specified item. Automatically manages accessibility focus actions.
    762      * <p>
    763      * Allows the implementing class to specify most node properties, but
    764      * overrides the following:
    765      * <ul>
    766      * <li>{@link AccessibilityNodeInfoCompat#setPackageName}
    767      * <li>{@link AccessibilityNodeInfoCompat#setClassName}
    768      * <li>{@link AccessibilityNodeInfoCompat#setParent(View)}
    769      * <li>{@link AccessibilityNodeInfoCompat#setSource(View, int)}
    770      * <li>{@link AccessibilityNodeInfoCompat#setVisibleToUser}
    771      * <li>{@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)}
    772      * </ul>
    773      * <p>
    774      * Uses the bounds of the parent view and the parent-relative bounding
    775      * rectangle specified by
    776      * {@link AccessibilityNodeInfoCompat#getBoundsInParent} to automatically
    777      * update the following properties:
    778      * <ul>
    779      * <li>{@link AccessibilityNodeInfoCompat#setVisibleToUser}
    780      * <li>{@link AccessibilityNodeInfoCompat#setBoundsInParent}
    781      * </ul>
    782      *
    783      * @param virtualViewId the virtual view id for item for which to construct
    784      *                      a node
    785      * @return an {@link AccessibilityNodeInfoCompat} for the specified item
    786      */
    787     @NonNull
    788     private AccessibilityNodeInfoCompat createNodeForChild(int virtualViewId) {
    789         final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain();
    790 
    791         // Ensure the client has good defaults.
    792         node.setEnabled(true);
    793         node.setFocusable(true);
    794         node.setClassName(DEFAULT_CLASS_NAME);
    795         node.setBoundsInParent(INVALID_PARENT_BOUNDS);
    796         node.setBoundsInScreen(INVALID_PARENT_BOUNDS);
    797         node.setParent(mHost);
    798 
    799         // Allow the client to populate the node.
    800         onPopulateNodeForVirtualView(virtualViewId, node);
    801 
    802         // Make sure the developer is following the rules.
    803         if ((node.getText() == null) && (node.getContentDescription() == null)) {
    804             throw new RuntimeException("Callbacks must add text or a content description in "
    805                     + "populateNodeForVirtualViewId()");
    806         }
    807 
    808         node.getBoundsInParent(mTempParentRect);
    809         if (mTempParentRect.equals(INVALID_PARENT_BOUNDS)) {
    810             throw new RuntimeException("Callbacks must set parent bounds in "
    811                     + "populateNodeForVirtualViewId()");
    812         }
    813 
    814         final int actions = node.getActions();
    815         if ((actions & AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS) != 0) {
    816             throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in "
    817                     + "populateNodeForVirtualViewId()");
    818         }
    819         if ((actions & AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) {
    820             throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in "
    821                     + "populateNodeForVirtualViewId()");
    822         }
    823 
    824         // Don't allow the client to override these properties.
    825         node.setPackageName(mHost.getContext().getPackageName());
    826         node.setSource(mHost, virtualViewId);
    827 
    828         // Manage internal accessibility focus state.
    829         if (mAccessibilityFocusedVirtualViewId == virtualViewId) {
    830             node.setAccessibilityFocused(true);
    831             node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
    832         } else {
    833             node.setAccessibilityFocused(false);
    834             node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
    835         }
    836 
    837         // Manage internal keyboard focus state.
    838         final boolean isFocused = mKeyboardFocusedVirtualViewId == virtualViewId;
    839         if (isFocused) {
    840             node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS);
    841         } else if (node.isFocusable()) {
    842             node.addAction(AccessibilityNodeInfoCompat.ACTION_FOCUS);
    843         }
    844         node.setFocused(isFocused);
    845 
    846         mHost.getLocationOnScreen(mTempGlobalRect);
    847 
    848         // If not explicitly specified, calculate screen-relative bounds and
    849         // offset for scroll position based on bounds in parent.
    850         node.getBoundsInScreen(mTempScreenRect);
    851         if (mTempScreenRect.equals(INVALID_PARENT_BOUNDS)) {
    852             node.getBoundsInParent(mTempScreenRect);
    853 
    854             // If there is a parent node, adjust bounds based on the parent node.
    855             if (node.mParentVirtualDescendantId != HOST_ID) {
    856                 AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain();
    857                 // Walk up the node tree to adjust the screen rect.
    858                 for (int virtualDescendantId = node.mParentVirtualDescendantId;
    859                         virtualDescendantId != HOST_ID;
    860                         virtualDescendantId = parentNode.mParentVirtualDescendantId) {
    861                     // Reset the values in the parent node we'll be using.
    862                     parentNode.setParent(mHost, HOST_ID);
    863                     parentNode.setBoundsInParent(INVALID_PARENT_BOUNDS);
    864                     // Adjust the bounds for the parent node.
    865                     onPopulateNodeForVirtualView(virtualDescendantId, parentNode);
    866                     parentNode.getBoundsInParent(mTempParentRect);
    867                     mTempScreenRect.offset(mTempParentRect.left, mTempParentRect.top);
    868                 }
    869                 parentNode.recycle();
    870             }
    871             // Adjust the rect for the host view's location.
    872             mTempScreenRect.offset(mTempGlobalRect[0] - mHost.getScrollX(),
    873                     mTempGlobalRect[1] - mHost.getScrollY());
    874         }
    875 
    876         if (mHost.getLocalVisibleRect(mTempVisibleRect)) {
    877             mTempVisibleRect.offset(mTempGlobalRect[0] - mHost.getScrollX(),
    878                     mTempGlobalRect[1] - mHost.getScrollY());
    879             final boolean intersects = mTempScreenRect.intersect(mTempVisibleRect);
    880             if (intersects) {
    881                 node.setBoundsInScreen(mTempScreenRect);
    882 
    883                 if (isVisibleToUser(mTempScreenRect)) {
    884                     node.setVisibleToUser(true);
    885                 }
    886             }
    887         }
    888 
    889         return node;
    890     }
    891 
    892     boolean performAction(int virtualViewId, int action, Bundle arguments) {
    893         switch (virtualViewId) {
    894             case HOST_ID:
    895                 return performActionForHost(action, arguments);
    896             default:
    897                 return performActionForChild(virtualViewId, action, arguments);
    898         }
    899     }
    900 
    901     private boolean performActionForHost(int action, Bundle arguments) {
    902         return ViewCompat.performAccessibilityAction(mHost, action, arguments);
    903     }
    904 
    905     private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) {
    906         switch (action) {
    907             case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
    908                 return requestAccessibilityFocus(virtualViewId);
    909             case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
    910                 return clearAccessibilityFocus(virtualViewId);
    911             case AccessibilityNodeInfoCompat.ACTION_FOCUS:
    912                 return requestKeyboardFocusForVirtualView(virtualViewId);
    913             case AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS:
    914                 return clearKeyboardFocusForVirtualView(virtualViewId);
    915             default:
    916                 return onPerformActionForVirtualView(virtualViewId, action, arguments);
    917         }
    918     }
    919 
    920     /**
    921      * Computes whether the specified {@link Rect} intersects with the visible
    922      * portion of its parent {@link View}. Modifies {@code localRect} to contain
    923      * only the visible portion.
    924      *
    925      * @param localRect a rectangle in local (parent) coordinates
    926      * @return whether the specified {@link Rect} is visible on the screen
    927      */
    928     private boolean isVisibleToUser(Rect localRect) {
    929         // Missing or empty bounds mean this view is not visible.
    930         if ((localRect == null) || localRect.isEmpty()) {
    931             return false;
    932         }
    933 
    934         // Attached to invisible window means this view is not visible.
    935         if (mHost.getWindowVisibility() != View.VISIBLE) {
    936             return false;
    937         }
    938 
    939         // An invisible predecessor means that this view is not visible.
    940         ViewParent viewParent = mHost.getParent();
    941         while (viewParent instanceof View) {
    942             final View view = (View) viewParent;
    943             if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) {
    944                 return false;
    945             }
    946             viewParent = view.getParent();
    947         }
    948 
    949         // A null parent implies the view is not visible.
    950         return viewParent != null;
    951     }
    952 
    953     /**
    954      * Attempts to give accessibility focus to a virtual view.
    955      * <p>
    956      * A virtual view will not actually take focus if
    957      * {@link AccessibilityManager#isEnabled()} returns false,
    958      * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false,
    959      * or the view already has accessibility focus.
    960      *
    961      * @param virtualViewId the identifier of the virtual view on which to
    962      *                      place accessibility focus
    963      * @return whether this virtual view actually took accessibility focus
    964      */
    965     private boolean requestAccessibilityFocus(int virtualViewId) {
    966         if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) {
    967             return false;
    968         }
    969         // TODO: Check virtual view visibility.
    970         if (mAccessibilityFocusedVirtualViewId != virtualViewId) {
    971             // Clear focus from the previously focused view, if applicable.
    972             if (mAccessibilityFocusedVirtualViewId != INVALID_ID) {
    973                 clearAccessibilityFocus(mAccessibilityFocusedVirtualViewId);
    974             }
    975 
    976             // Set focus on the new view.
    977             mAccessibilityFocusedVirtualViewId = virtualViewId;
    978 
    979             // TODO: Only invalidate virtual view bounds.
    980             mHost.invalidate();
    981             sendEventForVirtualView(virtualViewId,
    982                     AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
    983             return true;
    984         }
    985         return false;
    986     }
    987 
    988     /**
    989      * Attempts to clear accessibility focus from a virtual view.
    990      *
    991      * @param virtualViewId the identifier of the virtual view from which to
    992      *                      clear accessibility focus
    993      * @return whether this virtual view actually cleared accessibility focus
    994      */
    995     private boolean clearAccessibilityFocus(int virtualViewId) {
    996         if (mAccessibilityFocusedVirtualViewId == virtualViewId) {
    997             mAccessibilityFocusedVirtualViewId = INVALID_ID;
    998             mHost.invalidate();
    999             sendEventForVirtualView(virtualViewId,
   1000                     AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
   1001             return true;
   1002         }
   1003         return false;
   1004     }
   1005 
   1006     /**
   1007      * Attempts to give keyboard focus to a virtual view.
   1008      *
   1009      * @param virtualViewId the identifier of the virtual view on which to
   1010      *                      place keyboard focus
   1011      * @return whether this virtual view actually took keyboard focus
   1012      */
   1013     public final boolean requestKeyboardFocusForVirtualView(int virtualViewId) {
   1014         if (!mHost.isFocused() && !mHost.requestFocus()) {
   1015             // Host must have real keyboard focus.
   1016             return false;
   1017         }
   1018 
   1019         if (mKeyboardFocusedVirtualViewId == virtualViewId) {
   1020             // The virtual view already has focus.
   1021             return false;
   1022         }
   1023 
   1024         if (mKeyboardFocusedVirtualViewId != INVALID_ID) {
   1025             clearKeyboardFocusForVirtualView(mKeyboardFocusedVirtualViewId);
   1026         }
   1027 
   1028         mKeyboardFocusedVirtualViewId = virtualViewId;
   1029 
   1030         onVirtualViewKeyboardFocusChanged(virtualViewId, true);
   1031         sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_FOCUSED);
   1032 
   1033         return true;
   1034     }
   1035 
   1036     /**
   1037      * Attempts to clear keyboard focus from a virtual view.
   1038      *
   1039      * @param virtualViewId the identifier of the virtual view from which to
   1040      *                      clear keyboard focus
   1041      * @return whether this virtual view actually cleared keyboard focus
   1042      */
   1043     public final boolean clearKeyboardFocusForVirtualView(int virtualViewId) {
   1044         if (mKeyboardFocusedVirtualViewId != virtualViewId) {
   1045             // The virtual view is not focused.
   1046             return false;
   1047         }
   1048 
   1049         mKeyboardFocusedVirtualViewId = INVALID_ID;
   1050 
   1051         onVirtualViewKeyboardFocusChanged(virtualViewId, false);
   1052         sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_FOCUSED);
   1053 
   1054         return true;
   1055     }
   1056 
   1057     /**
   1058      * Provides a mapping between view-relative coordinates and logical
   1059      * items.
   1060      *
   1061      * @param x The view-relative x coordinate
   1062      * @param y The view-relative y coordinate
   1063      * @return virtual view identifier for the logical item under
   1064      *         coordinates (x,y) or {@link #HOST_ID} if there is no item at
   1065      *         the given coordinates
   1066      */
   1067     protected abstract int getVirtualViewAt(float x, float y);
   1068 
   1069     /**
   1070      * Populates a list with the view's visible items. The ordering of items
   1071      * within {@code virtualViewIds} specifies order of accessibility focus
   1072      * traversal.
   1073      *
   1074      * @param virtualViewIds The list to populate with visible items
   1075      */
   1076     protected abstract void getVisibleVirtualViews(List<Integer> virtualViewIds);
   1077 
   1078     /**
   1079      * Populates an {@link AccessibilityEvent} with information about the
   1080      * specified item.
   1081      * <p>
   1082      * The helper class automatically populates the following fields based on
   1083      * the values set by
   1084      * {@link #onPopulateNodeForVirtualView(int, AccessibilityNodeInfoCompat)},
   1085      * but implementations may optionally override them:
   1086      * <ul>
   1087      * <li>event text, see {@link AccessibilityEvent#getText()}
   1088      * <li>content description, see
   1089      * {@link AccessibilityEvent#setContentDescription(CharSequence)}
   1090      * <li>scrollability, see {@link AccessibilityEvent#setScrollable(boolean)}
   1091      * <li>password state, see {@link AccessibilityEvent#setPassword(boolean)}
   1092      * <li>enabled state, see {@link AccessibilityEvent#setEnabled(boolean)}
   1093      * <li>checked state, see {@link AccessibilityEvent#setChecked(boolean)}
   1094      * </ul>
   1095      * <p>
   1096      * The following required fields are automatically populated by the
   1097      * helper class and may not be overridden:
   1098      * <ul>
   1099      * <li>item class name, set to the value used in
   1100      * {@link #onPopulateNodeForVirtualView(int, AccessibilityNodeInfoCompat)}
   1101      * <li>package name, set to the package of the host view's
   1102      * {@link Context}, see {@link AccessibilityEvent#setPackageName}
   1103      * <li>event source, set to the host view and virtual view identifier,
   1104      * see {@link AccessibilityRecordCompat#setSource(AccessibilityRecord, View, int)}
   1105      * </ul>
   1106      *
   1107      * @param virtualViewId The virtual view id for the item for which to
   1108      *            populate the event
   1109      * @param event The event to populate
   1110      */
   1111     protected void onPopulateEventForVirtualView(int virtualViewId,
   1112             @NonNull AccessibilityEvent event) {
   1113         // Default implementation is no-op.
   1114     }
   1115 
   1116     /**
   1117      * Populates an {@link AccessibilityEvent} with information about the host
   1118      * view.
   1119      * <p>
   1120      * The default implementation is a no-op.
   1121      *
   1122      * @param event the event to populate with information about the host view
   1123      */
   1124     protected void onPopulateEventForHost(@NonNull AccessibilityEvent event) {
   1125         // Default implementation is no-op.
   1126     }
   1127 
   1128     /**
   1129      * Populates an {@link AccessibilityNodeInfoCompat} with information
   1130      * about the specified item.
   1131      * <p>
   1132      * Implementations <strong>must</strong> populate the following required
   1133      * fields:
   1134      * <ul>
   1135      * <li>event text, see
   1136      * {@link AccessibilityNodeInfoCompat#setText(CharSequence)} or
   1137      * {@link AccessibilityNodeInfoCompat#setContentDescription(CharSequence)}
   1138      * <li>bounds in parent coordinates, see
   1139      * {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)}
   1140      * </ul>
   1141      * <p>
   1142      * The helper class automatically populates the following fields with
   1143      * default values, but implementations may optionally override them:
   1144      * <ul>
   1145      * <li>enabled state, set to {@code true}, see
   1146      * {@link AccessibilityNodeInfoCompat#setEnabled(boolean)}
   1147      * <li>keyboard focusability, set to {@code true}, see
   1148      * {@link AccessibilityNodeInfoCompat#setFocusable(boolean)}
   1149      * <li>item class name, set to {@code android.view.View}, see
   1150      * {@link AccessibilityNodeInfoCompat#setClassName(CharSequence)}
   1151      * </ul>
   1152      * <p>
   1153      * The following required fields are automatically populated by the
   1154      * helper class and may not be overridden:
   1155      * <ul>
   1156      * <li>package name, identical to the package name set by
   1157      * {@link #onPopulateEventForVirtualView(int, AccessibilityEvent)}, see
   1158      * {@link AccessibilityNodeInfoCompat#setPackageName}
   1159      * <li>node source, identical to the event source set in
   1160      * {@link #onPopulateEventForVirtualView(int, AccessibilityEvent)}, see
   1161      * {@link AccessibilityNodeInfoCompat#setSource(View, int)}
   1162      * <li>parent view, set to the host view, see
   1163      * {@link AccessibilityNodeInfoCompat#setParent(View)}
   1164      * <li>visibility, computed based on parent-relative bounds, see
   1165      * {@link AccessibilityNodeInfoCompat#setVisibleToUser(boolean)}
   1166      * <li>accessibility focus, computed based on internal helper state, see
   1167      * {@link AccessibilityNodeInfoCompat#setAccessibilityFocused(boolean)}
   1168      * <li>keyboard focus, computed based on internal helper state, see
   1169      * {@link AccessibilityNodeInfoCompat#setFocused(boolean)}
   1170      * <li>bounds in screen coordinates, computed based on host view bounds,
   1171      * see {@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)}
   1172      * </ul>
   1173      * <p>
   1174      * Additionally, the helper class automatically handles keyboard focus and
   1175      * accessibility focus management by adding the appropriate
   1176      * {@link AccessibilityNodeInfoCompat#ACTION_FOCUS},
   1177      * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_FOCUS},
   1178      * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS}, or
   1179      * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
   1180      * actions. Implementations must <strong>never</strong> manually add these
   1181      * actions.
   1182      * <p>
   1183      * The helper class also automatically modifies parent- and
   1184      * screen-relative bounds to reflect the portion of the item visible
   1185      * within its parent.
   1186      *
   1187      * @param virtualViewId The virtual view identifier of the item for
   1188      *            which to populate the node
   1189      * @param node The node to populate
   1190      */
   1191     protected abstract void onPopulateNodeForVirtualView(
   1192             int virtualViewId, @NonNull AccessibilityNodeInfoCompat node);
   1193 
   1194     /**
   1195      * Populates an {@link AccessibilityNodeInfoCompat} with information
   1196      * about the host view.
   1197      * <p>
   1198      * The default implementation is a no-op.
   1199      *
   1200      * @param node the node to populate with information about the host view
   1201      */
   1202     protected void onPopulateNodeForHost(@NonNull AccessibilityNodeInfoCompat node) {
   1203         // Default implementation is no-op.
   1204     }
   1205 
   1206     /**
   1207      * Performs the specified accessibility action on the item associated
   1208      * with the virtual view identifier. See
   1209      * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)} for
   1210      * more information.
   1211      * <p>
   1212      * Implementations <strong>must</strong> handle any actions added manually
   1213      * in
   1214      * {@link #onPopulateNodeForVirtualView(int, AccessibilityNodeInfoCompat)}.
   1215      * <p>
   1216      * The helper class automatically handles focus management resulting
   1217      * from {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS}
   1218      * and
   1219      * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
   1220      * actions.
   1221      *
   1222      * @param virtualViewId The virtual view identifier of the item on which
   1223      *            to perform the action
   1224      * @param action The accessibility action to perform
   1225      * @param arguments (Optional) A bundle with additional arguments, or
   1226      *            null
   1227      * @return true if the action was performed
   1228      */
   1229     protected abstract boolean onPerformActionForVirtualView(
   1230             int virtualViewId, int action, @Nullable Bundle arguments);
   1231 
   1232     /**
   1233      * Exposes a virtual view hierarchy to the accessibility framework.
   1234      */
   1235     private class MyNodeProvider extends AccessibilityNodeProviderCompat {
   1236         MyNodeProvider() {
   1237         }
   1238 
   1239         @Override
   1240         public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
   1241             // The caller takes ownership of the node and is expected to
   1242             // recycle it when done, so always return a copy.
   1243             final AccessibilityNodeInfoCompat node =
   1244                     ExploreByTouchHelper.this.obtainAccessibilityNodeInfo(virtualViewId);
   1245             return AccessibilityNodeInfoCompat.obtain(node);
   1246         }
   1247 
   1248         @Override
   1249         public boolean performAction(int virtualViewId, int action, Bundle arguments) {
   1250             return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments);
   1251         }
   1252 
   1253         @Override
   1254         public AccessibilityNodeInfoCompat findFocus(int focusType) {
   1255             int focusedId = (focusType == AccessibilityNodeInfoCompat.FOCUS_ACCESSIBILITY)
   1256                     ? mAccessibilityFocusedVirtualViewId : mKeyboardFocusedVirtualViewId;
   1257             if (focusedId == INVALID_ID) {
   1258                 return null;
   1259             }
   1260             return createAccessibilityNodeInfo(focusedId);
   1261         }
   1262     }
   1263 }
   1264