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