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