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