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