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