ExploreByTouchHelper.java revision 6eb3cdf42d5382aef6b6a6afd7c305dbc27885b9
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     * Sets the currently hovered item, sending hover accessibility events as
213     * necessary to maintain the correct state.
214     *
215     * @param virtualViewId The virtual view id for the item currently being
216     *            hovered, or {@link #INVALID_ID} if no item is hovered within
217     *            the parent view.
218     */
219    private void updateHoveredVirtualView(int virtualViewId) {
220        if (mHoveredVirtualViewId == virtualViewId) {
221            return;
222        }
223
224        final int previousVirtualViewId = mHoveredVirtualViewId;
225        mHoveredVirtualViewId = virtualViewId;
226
227        // Stay consistent with framework behavior by sending ENTER/EXIT pairs
228        // in reverse order. This is accurate as of API 18.
229        sendEventForVirtualView(virtualViewId, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
230        sendEventForVirtualView(
231                previousVirtualViewId, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
232    }
233
234    /**
235     * Constructs and returns an {@link AccessibilityEvent} for the specified
236     * virtual view id, which includes the host view ({@link View#NO_ID}).
237     *
238     * @param virtualViewId The virtual view id for the item for which to
239     *            construct an event.
240     * @param eventType The type of event to construct.
241     * @return An {@link AccessibilityEvent} populated with information about
242     *         the specified item.
243     */
244    private AccessibilityEvent createEvent(int virtualViewId, int eventType) {
245        switch (virtualViewId) {
246            case View.NO_ID:
247                return createEventForHost(eventType);
248            default:
249                return createEventForChild(virtualViewId, eventType);
250        }
251    }
252
253    /**
254     * Constructs and returns an {@link AccessibilityEvent} for the host node.
255     *
256     * @param eventType The type of event to construct.
257     * @return An {@link AccessibilityEvent} populated with information about
258     *         the specified item.
259     */
260    private AccessibilityEvent createEventForHost(int eventType) {
261        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
262        ViewCompat.onInitializeAccessibilityEvent(mView, event);
263        return event;
264    }
265
266    /**
267     * Constructs and returns an {@link AccessibilityEvent} populated with
268     * information about the specified item.
269     *
270     * @param virtualViewId The virtual view id for the item for which to
271     *            construct an event.
272     * @param eventType The type of event to construct.
273     * @return An {@link AccessibilityEvent} populated with information about
274     *         the specified item.
275     */
276    private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) {
277        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
278        event.setEnabled(true);
279        event.setClassName(DEFAULT_CLASS_NAME);
280
281        // Allow the client to populate the event.
282        onPopulateEventForVirtualView(virtualViewId, event);
283
284        // Make sure the developer is following the rules.
285        if (event.getText().isEmpty() && (event.getContentDescription() == null)) {
286            throw new RuntimeException("Callbacks must add text or a content description in "
287                    + "populateEventForVirtualViewId()");
288        }
289
290        // Don't allow the client to override these properties.
291        event.setPackageName(mView.getContext().getPackageName());
292
293        final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
294        record.setSource(mView, virtualViewId);
295
296        return event;
297    }
298
299    /**
300     * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the
301     * specified virtual view id, which includes the host view
302     * ({@link View#NO_ID}).
303     *
304     * @param virtualViewId The virtual view id for the item for which to
305     *            construct a node.
306     * @return An {@link AccessibilityNodeInfoCompat} populated with information
307     *         about the specified item.
308     */
309    private AccessibilityNodeInfoCompat createNode(int virtualViewId) {
310        switch (virtualViewId) {
311            case View.NO_ID:
312                return createNodeForHost();
313            default:
314                return createNodeForChild(virtualViewId);
315        }
316    }
317
318    /**
319     * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the
320     * host view populated with its virtual descendants.
321     *
322     * @return An {@link AccessibilityNodeInfoCompat} for the parent node.
323     */
324    private AccessibilityNodeInfoCompat createNodeForHost() {
325        final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain(mView);
326        ViewCompat.onInitializeAccessibilityNodeInfo(mView, node);
327
328        // Add the virtual descendants.
329        final LinkedList<Integer> virtualViewIds = new LinkedList<Integer>();
330        getVisibleVirtualViews(virtualViewIds);
331
332        for (Integer childVirtualViewId : virtualViewIds) {
333            node.addChild(mView, childVirtualViewId);
334        }
335
336        return node;
337    }
338
339    /**
340     * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the
341     * specified item. Automatically manages accessibility focus actions.
342     * <p>
343     * Allows the implementing class to specify most node properties, but
344     * overrides the following:
345     * <ul>
346     * <li>{@link AccessibilityNodeInfoCompat#setPackageName}
347     * <li>{@link AccessibilityNodeInfoCompat#setClassName}
348     * <li>{@link AccessibilityNodeInfoCompat#setParent(View)}
349     * <li>{@link AccessibilityNodeInfoCompat#setSource(View, int)}
350     * <li>{@link AccessibilityNodeInfoCompat#setVisibleToUser}
351     * <li>{@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)}
352     * </ul>
353     * <p>
354     * Uses the bounds of the parent view and the parent-relative bounding
355     * rectangle specified by
356     * {@link AccessibilityNodeInfoCompat#getBoundsInParent} to automatically
357     * update the following properties:
358     * <ul>
359     * <li>{@link AccessibilityNodeInfoCompat#setVisibleToUser}
360     * <li>{@link AccessibilityNodeInfoCompat#setBoundsInParent}
361     * </ul>
362     *
363     * @param virtualViewId The virtual view id for item for which to construct
364     *            a node.
365     * @return An {@link AccessibilityNodeInfoCompat} for the specified item.
366     */
367    private AccessibilityNodeInfoCompat createNodeForChild(int virtualViewId) {
368        final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain();
369
370        // Ensure the client has good defaults.
371        node.setEnabled(true);
372        node.setClassName(DEFAULT_CLASS_NAME);
373
374        // Allow the client to populate the node.
375        onPopulateNodeForVirtualView(virtualViewId, node);
376
377        // Make sure the developer is following the rules.
378        if ((node.getText() == null) && (node.getContentDescription() == null)) {
379            throw new RuntimeException("Callbacks must add text or a content description in "
380                    + "populateNodeForVirtualViewId()");
381        }
382
383        node.getBoundsInParent(mTempParentRect);
384        if (mTempParentRect.isEmpty()) {
385            throw new RuntimeException("Callbacks must set parent bounds in "
386                    + "populateNodeForVirtualViewId()");
387        }
388
389        final int actions = node.getActions();
390        if ((actions & AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS) != 0) {
391            throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in "
392                    + "populateNodeForVirtualViewId()");
393        }
394        if ((actions & AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) {
395            throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in "
396                    + "populateNodeForVirtualViewId()");
397        }
398
399        // Don't allow the client to override these properties.
400        node.setPackageName(mView.getContext().getPackageName());
401        node.setSource(mView, virtualViewId);
402        node.setParent(mView);
403
404        // Manage internal accessibility focus state.
405        if (mFocusedVirtualViewId == virtualViewId) {
406            node.setAccessibilityFocused(true);
407            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
408        } else {
409            node.setAccessibilityFocused(false);
410            node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
411        }
412
413        // Set the visibility based on the parent bound.
414        if (intersectVisibleToUser(mTempParentRect)) {
415            node.setVisibleToUser(true);
416            node.setBoundsInParent(mTempParentRect);
417        }
418
419        // Calculate screen-relative bound.
420        mView.getLocationOnScreen(mTempGlobalRect);
421        final int offsetX = mTempGlobalRect[0];
422        final int offsetY = mTempGlobalRect[1];
423        mTempScreenRect.set(mTempParentRect);
424        mTempScreenRect.offset(offsetX, offsetY);
425        node.setBoundsInScreen(mTempScreenRect);
426
427        return node;
428    }
429
430    private boolean performAction(int virtualViewId, int action, Bundle arguments) {
431        switch (virtualViewId) {
432            case View.NO_ID:
433                return performActionForHost(action, arguments);
434            default:
435                return performActionForChild(virtualViewId, action, arguments);
436        }
437    }
438
439    private boolean performActionForHost(int action, Bundle arguments) {
440        return ViewCompat.performAccessibilityAction(mView, action, arguments);
441    }
442
443    private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) {
444        switch (action) {
445            case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
446            case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
447                return manageFocusForChild(virtualViewId, action, arguments);
448            default:
449                return onPerformActionForVirtualView(virtualViewId, action, arguments);
450        }
451    }
452
453    private boolean manageFocusForChild(int virtualViewId, int action, Bundle arguments) {
454        switch (action) {
455            case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
456                if (!isAccessibilityFocused(virtualViewId)) {
457                    return requestAccessibilityFocus(virtualViewId);
458                }
459                return false;
460            case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
461                if (isAccessibilityFocused(virtualViewId)) {
462                    clearAccessibilityFocus(virtualViewId);
463                    return true;
464                }
465                return false;
466            default:
467                return false;
468        }
469    }
470
471    /**
472     * Computes whether the specified {@link Rect} intersects with the visible
473     * portion of its parent {@link View}. Modifies {@code localRect} to contain
474     * only the visible portion.
475     *
476     * @param localRect A rectangle in local (parent) coordinates.
477     * @return Whether the specified {@link Rect} is visible on the screen.
478     */
479    private boolean intersectVisibleToUser(Rect localRect) {
480        // Missing or empty bounds mean this view is not visible.
481        if ((localRect == null) || localRect.isEmpty()) {
482            return false;
483        }
484
485        // Attached to invisible window means this view is not visible.
486        if (mView.getWindowVisibility() != View.VISIBLE) {
487            return false;
488        }
489
490        // An invisible predecessor means that this view is not visible.
491        Object viewParent = this;
492        while (viewParent instanceof View) {
493            final View view = (View) viewParent;
494            if ((ViewCompat.getAlpha(view) <= 0) || (view.getVisibility() != View.VISIBLE)) {
495                return false;
496            }
497            viewParent = view.getParent();
498        }
499
500        // A null parent implies the view is not visible.
501        if (viewParent == null) {
502            return false;
503        }
504
505        // If no portion of the parent is visible, this view is not visible.
506        if (!mView.getLocalVisibleRect(mTempVisibleRect)) {
507            return false;
508        }
509
510        // Check if the view intersects the visible portion of the parent.
511        return localRect.intersect(mTempVisibleRect);
512    }
513
514    /**
515     * Returns whether this virtual view is accessibility focused.
516     *
517     * @return True if the view is accessibility focused.
518     */
519    private boolean isAccessibilityFocused(int virtualViewId) {
520        return (mFocusedVirtualViewId == virtualViewId);
521    }
522
523    /**
524     * Attempts to give accessibility focus to a virtual view.
525     * <p>
526     * A virtual view will not actually take focus if
527     * {@link AccessibilityManager#isEnabled()} returns false,
528     * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false,
529     * or the view already has accessibility focus.
530     *
531     * @param virtualViewId The id of the virtual view on which to place
532     *            accessibility focus.
533     * @return Whether this virtual view actually took accessibility focus.
534     */
535    private boolean requestAccessibilityFocus(int virtualViewId) {
536        if (!mManager.isEnabled()
537                || !AccessibilityManagerCompat.isTouchExplorationEnabled(mManager)) {
538            return false;
539        }
540        // TODO: Check virtual view visibility.
541        if (!isAccessibilityFocused(virtualViewId)) {
542            mFocusedVirtualViewId = virtualViewId;
543            // TODO: Only invalidate virtual view bounds.
544            mView.invalidate();
545            sendEventForVirtualView(virtualViewId,
546                    AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
547            notifyAccessibilityStateChanged(virtualViewId);
548            return true;
549        }
550        return false;
551    }
552
553    /**
554     * Attempts to clear accessibility focus from a virtual view.
555     */
556    private void clearAccessibilityFocus(int virtualViewId) {
557        if (isAccessibilityFocused(virtualViewId)) {
558            mFocusedVirtualViewId = INVALID_ID;
559            mView.invalidate();
560            sendEventForVirtualView(virtualViewId,
561                    AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
562            notifyAccessibilityStateChanged(virtualViewId);
563        }
564    }
565
566    private void notifyAccessibilityStateChanged(int virtualViewId) {
567        // TODO: This method is not visible.
568    }
569
570    /**
571     * Provides a mapping between view-relative coordinates and logical
572     * items.
573     *
574     * @param x The view-relative x coordinate
575     * @param y The view-relative y coordinate
576     * @return virtual view identifier for the logical item under
577     *         coordinates (x,y)
578     */
579    protected abstract int getVirtualViewAt(float x, float y);
580
581    /**
582     * Populates a list with the view's visible items. The ordering of items
583     * within {@code virtualViewIds} specifies order of accessibility focus
584     * traversal.
585     *
586     * @param virtualViewIds The list to populate with visible items
587     */
588    protected abstract void getVisibleVirtualViews(List<Integer> virtualViewIds);
589
590    /**
591     * Populates an {@link AccessibilityEvent} with information about the
592     * specified item.
593     * <p>
594     * Implementations <b>must</b> populate the following required fields:
595     * <ul>
596     * <li>event text, see {@link AccessibilityEvent#getText} or
597     * {@link AccessibilityEvent#setContentDescription}
598     * </ul>
599     * <p>
600     * The helper class automatically populates the following fields with
601     * default values, but implementations may optionally override them:
602     * <ul>
603     * <li>item class name, set to android.view.View, see
604     * {@link AccessibilityEvent#setClassName}
605     * </ul>
606     * <p>
607     * The following required fields are automatically populated by the
608     * helper class and may not be overridden:
609     * <ul>
610     * <li>package name, set to the package of the host view's
611     * {@link Context}, see {@link AccessibilityEvent#setPackageName}
612     * <li>event source, set to the host view and virtual view identifier,
613     * see {@link AccessibilityRecordCompat#setSource(View, int)}
614     * </ul>
615     *
616     * @param virtualViewId The virtual view id for the item for which to
617     *            populate the event
618     * @param event The event to populate
619     */
620    protected abstract void onPopulateEventForVirtualView(
621            int virtualViewId, AccessibilityEvent event);
622
623    /**
624     * Populates an {@link AccessibilityNodeInfoCompat} with information
625     * about the specified item.
626     * <p>
627     * Implementations <b>must</b> populate the following required fields:
628     * <ul>
629     * <li>event text, see {@link AccessibilityNodeInfoCompat#setText} or
630     * {@link AccessibilityNodeInfoCompat#setContentDescription}
631     * <li>bounds in parent coordinates, see
632     * {@link AccessibilityNodeInfoCompat#setBoundsInParent}
633     * </ul>
634     * <p>
635     * The helper class automatically populates the following fields with
636     * default values, but implementations may optionally override them:
637     * <ul>
638     * <li>enabled state, set to true, see
639     * {@link AccessibilityNodeInfoCompat#setEnabled}
640     * <li>item class name, identical to the class name set by
641     * {@link #onPopulateEventForVirtualView}, see
642     * {@link AccessibilityNodeInfoCompat#setClassName}
643     * </ul>
644     * <p>
645     * The following required fields are automatically populated by the
646     * helper class and may not be overridden:
647     * <ul>
648     * <li>package name, identical to the package name set by
649     * {@link #onPopulateEventForVirtualView}, see
650     * {@link AccessibilityNodeInfoCompat#setPackageName}
651     * <li>node source, identical to the event source set in
652     * {@link #onPopulateEventForVirtualView}, see
653     * {@link AccessibilityNodeInfoCompat#setSource(View, int)}
654     * <li>parent view, set to the host view, see
655     * {@link AccessibilityNodeInfoCompat#setParent(View)}
656     * <li>visibility, computed based on parent-relative bounds, see
657     * {@link AccessibilityNodeInfoCompat#setVisibleToUser}
658     * <li>accessibility focus, computed based on internal helper state, see
659     * {@link AccessibilityNodeInfoCompat#setAccessibilityFocused}
660     * <li>bounds in screen coordinates, computed based on host view bounds,
661     * see {@link AccessibilityNodeInfoCompat#setBoundsInScreen}
662     * </ul>
663     * <p>
664     * Additionally, the helper class automatically handles accessibility
665     * focus management by adding the appropriate
666     * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} or
667     * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
668     * action. Implementations must <b>never</b> manually add these actions.
669     * <p>
670     * The helper class also automatically modifies parent- and
671     * screen-relative bounds to reflect the portion of the item visible
672     * within its parent.
673     *
674     * @param virtualViewId The virtual view identifier of the item for
675     *            which to populate the node
676     * @param node The node to populate
677     */
678    protected abstract void onPopulateNodeForVirtualView(
679            int virtualViewId, AccessibilityNodeInfoCompat node);
680
681    /**
682     * Performs the specified accessibility action on the item associated
683     * with the virtual view identifier. See
684     * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)} for
685     * more information.
686     * <p>
687     * Implementations <b>must</b> handle any actions added manually in
688     * {@link #onPopulateNodeForVirtualView}.
689     * <p>
690     * The helper class automatically handles focus management resulting
691     * from {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS}
692     * and
693     * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
694     * actions.
695     *
696     * @param virtualViewId The virtual view identifier of the item on which
697     *            to perform the action
698     * @param action The accessibility action to perform
699     * @param arguments (Optional) A bundle with additional arguments, or
700     *            null
701     * @return true if the action was performed
702     */
703    protected abstract boolean onPerformActionForVirtualView(
704            int virtualViewId, int action, Bundle arguments);
705
706    /**
707     * Exposes a virtual view hierarchy to the accessibility framework. Only
708     * used in API 16+.
709     */
710    private class ExploreByTouchNodeProvider extends AccessibilityNodeProviderCompat {
711        @Override
712        public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
713            return ExploreByTouchHelper.this.createNode(virtualViewId);
714        }
715
716        @Override
717        public boolean performAction(int virtualViewId, int action, Bundle arguments) {
718            return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments);
719        }
720    }
721}
722