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