ExploreByTouchHelper.java revision 7a81bd82f4d05e6843e4a9743e194cceb0c2d2e1
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 com.android.internal.widget;
18
19import android.content.Context;
20import android.graphics.Rect;
21import android.os.Bundle;
22import android.view.accessibility.*;
23import android.view.MotionEvent;
24import android.view.View;
25import android.view.ViewParent;
26import android.view.accessibility.AccessibilityEvent;
27import android.view.accessibility.AccessibilityManager;
28import android.view.accessibility.AccessibilityNodeInfo;
29import android.view.accessibility.AccessibilityNodeProvider;
30
31import java.util.LinkedList;
32import java.util.List;
33
34/**
35 * ExploreByTouchHelper is a utility class for implementing accessibility
36 * support in custom {@link android.view.View}s that represent a collection of View-like
37 * logical items. It extends {@link android.view.accessibility.AccessibilityNodeProvider} and
38 * simplifies many aspects of providing information to accessibility services
39 * and managing accessibility focus. This class does not currently support
40 * hierarchies of logical items.
41 * <p>
42 * This should be applied to the parent view using
43 * {@link android.view.View#setAccessibilityDelegate}:
44 *
45 * <pre>
46 * mAccessHelper = ExploreByTouchHelper.create(someView, mAccessHelperCallback);
47 * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper);
48 * </pre>
49 */
50public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {
51    /** Virtual node identifier value for invalid nodes. */
52    public static final int INVALID_ID = Integer.MIN_VALUE;
53
54    /** Default class name used for virtual views. */
55    private static final String DEFAULT_CLASS_NAME = View.class.getName();
56
57    /** Default bounds used to determine if the client didn't set any. */
58    private static final Rect INVALID_PARENT_BOUNDS = new Rect(
59            Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
60
61    // Temporary, reusable data structures.
62    private final Rect mTempScreenRect = new Rect();
63    private final Rect mTempParentRect = new Rect();
64    private final Rect mTempVisibleRect = new Rect();
65    private final int[] mTempGlobalRect = new int[2];
66
67    /** View's context **/
68    private Context mContext;
69
70    /** System accessibility manager, used to check state and send events. */
71    private final AccessibilityManager mManager;
72
73    /** View whose internal structure is exposed through this helper. */
74    private final View mView;
75
76    /** Node provider that handles creating nodes and performing actions. */
77    private ExploreByTouchNodeProvider mNodeProvider;
78
79    /** Virtual view id for the currently focused logical item. */
80    private int mFocusedVirtualViewId = INVALID_ID;
81
82    /** Virtual view id for the currently hovered logical item. */
83    private int mHoveredVirtualViewId = INVALID_ID;
84
85    /**
86     * Factory method to create a new {@link ExploreByTouchHelper}.
87     *
88     * @param forView View whose logical children are exposed by this helper.
89     */
90    public ExploreByTouchHelper(View forView) {
91        if (forView == null) {
92            throw new IllegalArgumentException("View may not be null");
93        }
94
95        mView = forView;
96        mContext = forView.getContext();
97        mManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
98    }
99
100    /**
101     * Returns the {@link android.view.accessibility.AccessibilityNodeProvider} for this helper.
102     *
103     * @param host View whose logical children are exposed by this helper.
104     * @return The accessibility node provider for this helper.
105     */
106    @Override
107    public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
108        if (mNodeProvider == null) {
109            mNodeProvider = new ExploreByTouchNodeProvider();
110        }
111        return mNodeProvider;
112    }
113
114    /**
115     * Dispatches hover {@link android.view.MotionEvent}s to the virtual view hierarchy when
116     * the Explore by Touch feature is enabled.
117     * <p>
118     * This method should be called by overriding
119     * {@link View#dispatchHoverEvent}:
120     *
121     * <pre>&#64;Override
122     * public boolean dispatchHoverEvent(MotionEvent event) {
123     *   if (mHelper.dispatchHoverEvent(this, event) {
124     *     return true;
125     *   }
126     *   return super.dispatchHoverEvent(event);
127     * }
128     * </pre>
129     *
130     * @param event The hover event to dispatch to the virtual view hierarchy.
131     * @return Whether the hover event was handled.
132     */
133    public boolean dispatchHoverEvent(MotionEvent event) {
134        if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) {
135            return false;
136        }
137
138        switch (event.getAction()) {
139            case MotionEvent.ACTION_HOVER_MOVE:
140            case MotionEvent.ACTION_HOVER_ENTER:
141                final int virtualViewId = getVirtualViewAt(event.getX(), event.getY());
142                updateHoveredVirtualView(virtualViewId);
143                return (virtualViewId != INVALID_ID);
144            case MotionEvent.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 parent.requestSendAccessibilityEvent(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(virtualViewId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
208    }
209
210    /**
211     * Returns the virtual view id for the currently focused item,
212     *
213     * @return A virtual view id, or {@link #INVALID_ID} if no item is
214     *         currently focused.
215     */
216    public int getFocusedVirtualView() {
217        return mFocusedVirtualViewId;
218    }
219
220    /**
221     * Sets the currently hovered item, sending hover accessibility events as
222     * necessary to maintain the correct state.
223     *
224     * @param virtualViewId The virtual view id for the item currently being
225     *            hovered, or {@link #INVALID_ID} if no item is hovered within
226     *            the parent view.
227     */
228    private void updateHoveredVirtualView(int virtualViewId) {
229        if (mHoveredVirtualViewId == virtualViewId) {
230            return;
231        }
232
233        final int previousVirtualViewId = mHoveredVirtualViewId;
234        mHoveredVirtualViewId = virtualViewId;
235
236        // Stay consistent with framework behavior by sending ENTER/EXIT pairs
237        // in reverse order. This is accurate as of API 18.
238        sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
239        sendEventForVirtualView(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
240    }
241
242    /**
243     * Constructs and returns an {@link AccessibilityEvent} for the specified
244     * virtual view id, which includes the host view ({@link View#NO_ID}).
245     *
246     * @param virtualViewId The virtual view id for the item for which to
247     *            construct an event.
248     * @param eventType The type of event to construct.
249     * @return An {@link AccessibilityEvent} populated with information about
250     *         the specified item.
251     */
252    private AccessibilityEvent createEvent(int virtualViewId, int eventType) {
253        switch (virtualViewId) {
254            case View.NO_ID:
255                return createEventForHost(eventType);
256            default:
257                return createEventForChild(virtualViewId, eventType);
258        }
259    }
260
261    /**
262     * Constructs and returns an {@link AccessibilityEvent} for the host node.
263     *
264     * @param eventType The type of event to construct.
265     * @return An {@link AccessibilityEvent} populated with information about
266     *         the specified item.
267     */
268    private AccessibilityEvent createEventForHost(int eventType) {
269        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
270        onInitializeAccessibilityEvent(mView, event);
271        return event;
272    }
273
274    /**
275     * Constructs and returns an {@link AccessibilityEvent} populated with
276     * information about the specified item.
277     *
278     * @param virtualViewId The virtual view id for the item for which to
279     *            construct an event.
280     * @param eventType The type of event to construct.
281     * @return An {@link AccessibilityEvent} populated with information about
282     *         the specified item.
283     */
284    private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) {
285        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
286        event.setEnabled(true);
287        event.setClassName(DEFAULT_CLASS_NAME);
288
289        // Allow the client to populate the event.
290        onPopulateEventForVirtualView(virtualViewId, event);
291
292        // Make sure the developer is following the rules.
293        if (event.getText().isEmpty() && (event.getContentDescription() == null)) {
294            throw new RuntimeException("Callbacks must add text or a content description in "
295                    + "populateEventForVirtualViewId()");
296        }
297
298        // Don't allow the client to override these properties.
299        event.setPackageName(mView.getContext().getPackageName());
300        event.setSource(mView, virtualViewId);
301
302        return event;
303    }
304
305    /**
306     * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the
307     * specified virtual view id, which includes the host view
308     * ({@link View#NO_ID}).
309     *
310     * @param virtualViewId The virtual view id for the item for which to
311     *            construct a node.
312     * @return An {@link android.view.accessibility.AccessibilityNodeInfo} populated with information
313     *         about the specified item.
314     */
315    private AccessibilityNodeInfo createNode(int virtualViewId) {
316        switch (virtualViewId) {
317            case View.NO_ID:
318                return createNodeForHost();
319            default:
320                return createNodeForChild(virtualViewId);
321        }
322    }
323
324    /**
325     * Constructs and returns an {@link AccessibilityNodeInfo} for the
326     * host view populated with its virtual descendants.
327     *
328     * @return An {@link AccessibilityNodeInfo} for the parent node.
329     */
330    private AccessibilityNodeInfo createNodeForHost() {
331        final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView);
332        onInitializeAccessibilityNodeInfo(mView, node);
333
334        // Add the virtual descendants.
335        final LinkedList<Integer> virtualViewIds = new LinkedList<Integer>();
336        getVisibleVirtualViews(virtualViewIds);
337
338        for (Integer childVirtualViewId : virtualViewIds) {
339            node.addChild(mView, childVirtualViewId);
340        }
341
342        return node;
343    }
344
345    /**
346     * Constructs and returns an {@link AccessibilityNodeInfo} for the
347     * specified item. Automatically manages accessibility focus actions.
348     * <p>
349     * Allows the implementing class to specify most node properties, but
350     * overrides the following:
351     * <ul>
352     * <li>{@link AccessibilityNodeInfo#setPackageName}
353     * <li>{@link AccessibilityNodeInfo#setClassName}
354     * <li>{@link AccessibilityNodeInfo#setParent(View)}
355     * <li>{@link AccessibilityNodeInfo#setSource(View, int)}
356     * <li>{@link AccessibilityNodeInfo#setVisibleToUser}
357     * <li>{@link AccessibilityNodeInfo#setBoundsInScreen(Rect)}
358     * </ul>
359     * <p>
360     * Uses the bounds of the parent view and the parent-relative bounding
361     * rectangle specified by
362     * {@link AccessibilityNodeInfo#getBoundsInParent} to automatically
363     * update the following properties:
364     * <ul>
365     * <li>{@link AccessibilityNodeInfo#setVisibleToUser}
366     * <li>{@link AccessibilityNodeInfo#setBoundsInParent}
367     * </ul>
368     *
369     * @param virtualViewId The virtual view id for item for which to construct
370     *            a node.
371     * @return An {@link AccessibilityNodeInfo} for the specified item.
372     */
373    private AccessibilityNodeInfo createNodeForChild(int virtualViewId) {
374        final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
375
376        // Ensure the client has good defaults.
377        node.setEnabled(true);
378        node.setClassName(DEFAULT_CLASS_NAME);
379        node.setBoundsInParent(INVALID_PARENT_BOUNDS);
380
381        // Allow the client to populate the node.
382        onPopulateNodeForVirtualView(virtualViewId, node);
383
384        // Make sure the developer is following the rules.
385        if ((node.getText() == null) && (node.getContentDescription() == null)) {
386            throw new RuntimeException("Callbacks must add text or a content description in "
387                    + "populateNodeForVirtualViewId()");
388        }
389
390        node.getBoundsInParent(mTempParentRect);
391        if (mTempParentRect.equals(INVALID_PARENT_BOUNDS)) {
392            throw new RuntimeException("Callbacks must set parent bounds in "
393                    + "populateNodeForVirtualViewId()");
394        }
395
396        final int actions = node.getActions();
397        if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) {
398            throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in "
399                    + "populateNodeForVirtualViewId()");
400        }
401        if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) {
402            throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in "
403                    + "populateNodeForVirtualViewId()");
404        }
405
406        // Don't allow the client to override these properties.
407        node.setPackageName(mView.getContext().getPackageName());
408        node.setSource(mView, virtualViewId);
409        node.setParent(mView);
410
411        // Manage internal accessibility focus state.
412        if (mFocusedVirtualViewId == virtualViewId) {
413            node.setAccessibilityFocused(true);
414            node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
415        } else {
416            node.setAccessibilityFocused(false);
417            node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
418        }
419
420        // Set the visibility based on the parent bound.
421        if (intersectVisibleToUser(mTempParentRect)) {
422            node.setVisibleToUser(true);
423            node.setBoundsInParent(mTempParentRect);
424        }
425
426        // Calculate screen-relative bound.
427        mView.getLocationOnScreen(mTempGlobalRect);
428        final int offsetX = mTempGlobalRect[0];
429        final int offsetY = mTempGlobalRect[1];
430        mTempScreenRect.set(mTempParentRect);
431        mTempScreenRect.offset(offsetX, offsetY);
432        node.setBoundsInScreen(mTempScreenRect);
433
434        return node;
435    }
436
437    private boolean performAction(int virtualViewId, int action, Bundle arguments) {
438        switch (virtualViewId) {
439            case View.NO_ID:
440                return performActionForHost(action, arguments);
441            default:
442                return performActionForChild(virtualViewId, action, arguments);
443        }
444    }
445
446    private boolean performActionForHost(int action, Bundle arguments) {
447        return performAccessibilityAction(mView, action, arguments);
448    }
449
450    private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) {
451        switch (action) {
452            case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
453            case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
454                return manageFocusForChild(virtualViewId, action, arguments);
455            default:
456                return onPerformActionForVirtualView(virtualViewId, action, arguments);
457        }
458    }
459
460    private boolean manageFocusForChild(int virtualViewId, int action, Bundle arguments) {
461        switch (action) {
462            case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
463                return requestAccessibilityFocus(virtualViewId);
464            case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
465                return clearAccessibilityFocus(virtualViewId);
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        ViewParent viewParent = mView.getParent();
492        while (viewParent instanceof View) {
493            final View view = (View) viewParent;
494            if ((view.getAlpha() <= 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        final AccessibilityManager accessibilityManager =
537                (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
538
539        if (!mManager.isEnabled()
540                || !accessibilityManager.isTouchExplorationEnabled()) {
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                    AccessibilityEvent.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                    AccessibilityEvent.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 AccessibilityRecord#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 AccessibilityNodeInfo} 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 AccessibilityNodeInfo#setText} or
633     * {@link AccessibilityNodeInfo#setContentDescription}
634     * <li>bounds in parent coordinates, see
635     * {@link AccessibilityNodeInfo#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 AccessibilityNodeInfo#setEnabled}
643     * <li>item class name, identical to the class name set by
644     * {@link #onPopulateEventForVirtualView}, see
645     * {@link AccessibilityNodeInfo#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 AccessibilityNodeInfo#setPackageName}
654     * <li>node source, identical to the event source set in
655     * {@link #onPopulateEventForVirtualView}, see
656     * {@link AccessibilityNodeInfo#setSource(View, int)}
657     * <li>parent view, set to the host view, see
658     * {@link AccessibilityNodeInfo#setParent(View)}
659     * <li>visibility, computed based on parent-relative bounds, see
660     * {@link AccessibilityNodeInfo#setVisibleToUser}
661     * <li>accessibility focus, computed based on internal helper state, see
662     * {@link AccessibilityNodeInfo#setAccessibilityFocused}
663     * <li>bounds in screen coordinates, computed based on host view bounds,
664     * see {@link AccessibilityNodeInfo#setBoundsInScreen}
665     * </ul>
666     * <p>
667     * Additionally, the helper class automatically handles accessibility
668     * focus management by adding the appropriate
669     * {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} or
670     * {@link AccessibilityNodeInfo#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, AccessibilityNodeInfo node);
683
684    /**
685     * Performs the specified accessibility action on the item associated
686     * with the virtual view identifier. See
687     * {@link AccessibilityNodeInfo#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 AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS}
695     * and
696     * {@link AccessibilityNodeInfo#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 AccessibilityNodeProvider {
714        @Override
715        public AccessibilityNodeInfo 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