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