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_SUBTREE}
233      *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
234      *         <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
235      *         </ul>
236      */
invalidateVirtualView(int virtualViewId, int changeTypes)237     public void invalidateVirtualView(int virtualViewId, int changeTypes) {
238         if (virtualViewId != INVALID_ID && mManager.isEnabled()) {
239             final ViewParent parent = mView.getParent();
240             if (parent != null) {
241                 final AccessibilityEvent event = createEvent(virtualViewId,
242                         AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
243                 event.setContentChangeTypes(changeTypes);
244                 parent.requestSendAccessibilityEvent(mView, event);
245             }
246         }
247     }
248 
249     /**
250      * Returns the virtual view id for the currently focused item,
251      *
252      * @return A virtual view id, or {@link #INVALID_ID} if no item is
253      *         currently focused.
254      */
getFocusedVirtualView()255     public int getFocusedVirtualView() {
256         return mFocusedVirtualViewId;
257     }
258 
259     /**
260      * Sets the currently hovered item, sending hover accessibility events as
261      * necessary to maintain the correct state.
262      *
263      * @param virtualViewId The virtual view id for the item currently being
264      *            hovered, or {@link #INVALID_ID} if no item is hovered within
265      *            the parent view.
266      */
updateHoveredVirtualView(int virtualViewId)267     private void updateHoveredVirtualView(int virtualViewId) {
268         if (mHoveredVirtualViewId == virtualViewId) {
269             return;
270         }
271 
272         final int previousVirtualViewId = mHoveredVirtualViewId;
273         mHoveredVirtualViewId = virtualViewId;
274 
275         // Stay consistent with framework behavior by sending ENTER/EXIT pairs
276         // in reverse order. This is accurate as of API 18.
277         sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
278         sendEventForVirtualView(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
279     }
280 
281     /**
282      * Constructs and returns an {@link AccessibilityEvent} for the specified
283      * virtual view id, which includes the host view ({@link #HOST_ID}).
284      *
285      * @param virtualViewId The virtual view id for the item for which to
286      *            construct an event.
287      * @param eventType The type of event to construct.
288      * @return An {@link AccessibilityEvent} populated with information about
289      *         the specified item.
290      */
createEvent(int virtualViewId, int eventType)291     private AccessibilityEvent createEvent(int virtualViewId, int eventType) {
292         switch (virtualViewId) {
293             case HOST_ID:
294                 return createEventForHost(eventType);
295             default:
296                 return createEventForChild(virtualViewId, eventType);
297         }
298     }
299 
300     /**
301      * Constructs and returns an {@link AccessibilityEvent} for the host node.
302      *
303      * @param eventType The type of event to construct.
304      * @return An {@link AccessibilityEvent} populated with information about
305      *         the specified item.
306      */
createEventForHost(int eventType)307     private AccessibilityEvent createEventForHost(int eventType) {
308         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
309         mView.onInitializeAccessibilityEvent(event);
310 
311         // Allow the client to populate the event.
312         onPopulateEventForHost(event);
313 
314         return event;
315     }
316 
317     /**
318      * Constructs and returns an {@link AccessibilityEvent} populated with
319      * information about the specified item.
320      *
321      * @param virtualViewId The virtual view id for the item for which to
322      *            construct an event.
323      * @param eventType The type of event to construct.
324      * @return An {@link AccessibilityEvent} populated with information about
325      *         the specified item.
326      */
createEventForChild(int virtualViewId, int eventType)327     private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) {
328         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
329         event.setEnabled(true);
330         event.setClassName(DEFAULT_CLASS_NAME);
331 
332         // Allow the client to populate the event.
333         onPopulateEventForVirtualView(virtualViewId, event);
334 
335         // Make sure the developer is following the rules.
336         if (event.getText().isEmpty() && (event.getContentDescription() == null)) {
337             throw new RuntimeException("Callbacks must add text or a content description in "
338                     + "populateEventForVirtualViewId()");
339         }
340 
341         // Don't allow the client to override these properties.
342         event.setPackageName(mView.getContext().getPackageName());
343         event.setSource(mView, virtualViewId);
344 
345         return event;
346     }
347 
348     /**
349      * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the
350      * specified virtual view id, which includes the host view
351      * ({@link #HOST_ID}).
352      *
353      * @param virtualViewId The virtual view id for the item for which to
354      *            construct a node.
355      * @return An {@link android.view.accessibility.AccessibilityNodeInfo} populated with information
356      *         about the specified item.
357      */
createNode(int virtualViewId)358     private AccessibilityNodeInfo createNode(int virtualViewId) {
359         switch (virtualViewId) {
360             case HOST_ID:
361                 return createNodeForHost();
362             default:
363                 return createNodeForChild(virtualViewId);
364         }
365     }
366 
367     /**
368      * Constructs and returns an {@link AccessibilityNodeInfo} for the
369      * host view populated with its virtual descendants.
370      *
371      * @return An {@link AccessibilityNodeInfo} for the parent node.
372      */
createNodeForHost()373     private AccessibilityNodeInfo createNodeForHost() {
374         final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView);
375         mView.onInitializeAccessibilityNodeInfo(node);
376         final int realNodeCount = node.getChildCount();
377 
378         // Allow the client to populate the host node.
379         onPopulateNodeForHost(node);
380 
381         // Add the virtual descendants.
382         if (mTempArray == null) {
383             mTempArray = new IntArray();
384         } else {
385             mTempArray.clear();
386         }
387         final IntArray virtualViewIds = mTempArray;
388         getVisibleVirtualViews(virtualViewIds);
389         if (realNodeCount > 0 && virtualViewIds.size() > 0) {
390             throw new RuntimeException("Views cannot have both real and virtual children");
391         }
392 
393         final int N = virtualViewIds.size();
394         for (int i = 0; i < N; i++) {
395             node.addChild(mView, virtualViewIds.get(i));
396         }
397 
398         return node;
399     }
400 
401     /**
402      * Constructs and returns an {@link AccessibilityNodeInfo} for the
403      * specified item. Automatically manages accessibility focus actions.
404      * <p>
405      * Allows the implementing class to specify most node properties, but
406      * overrides the following:
407      * <ul>
408      * <li>{@link AccessibilityNodeInfo#setPackageName}
409      * <li>{@link AccessibilityNodeInfo#setClassName}
410      * <li>{@link AccessibilityNodeInfo#setParent(View)}
411      * <li>{@link AccessibilityNodeInfo#setSource(View, int)}
412      * <li>{@link AccessibilityNodeInfo#setVisibleToUser}
413      * <li>{@link AccessibilityNodeInfo#setBoundsInScreen(Rect)}
414      * </ul>
415      * <p>
416      * Uses the bounds of the parent view and the parent-relative bounding
417      * rectangle specified by
418      * {@link AccessibilityNodeInfo#getBoundsInParent} to automatically
419      * update the following properties:
420      * <ul>
421      * <li>{@link AccessibilityNodeInfo#setVisibleToUser}
422      * <li>{@link AccessibilityNodeInfo#setBoundsInParent}
423      * </ul>
424      *
425      * @param virtualViewId The virtual view id for item for which to construct
426      *            a node.
427      * @return An {@link AccessibilityNodeInfo} for the specified item.
428      */
createNodeForChild(int virtualViewId)429     private AccessibilityNodeInfo createNodeForChild(int virtualViewId) {
430         ensureTempRects();
431         final Rect tempParentRect = mTempParentRect;
432         final int[] tempGlobalRect = mTempGlobalRect;
433         final Rect tempScreenRect = mTempScreenRect;
434 
435         final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
436 
437         // Ensure the client has good defaults.
438         node.setEnabled(true);
439         node.setClassName(DEFAULT_CLASS_NAME);
440         node.setBoundsInParent(INVALID_PARENT_BOUNDS);
441 
442         // Allow the client to populate the node.
443         onPopulateNodeForVirtualView(virtualViewId, node);
444 
445         // Make sure the developer is following the rules.
446         if ((node.getText() == null) && (node.getContentDescription() == null)) {
447             throw new RuntimeException("Callbacks must add text or a content description in "
448                     + "populateNodeForVirtualViewId()");
449         }
450 
451         node.getBoundsInParent(tempParentRect);
452         if (tempParentRect.equals(INVALID_PARENT_BOUNDS)) {
453             throw new RuntimeException("Callbacks must set parent bounds in "
454                     + "populateNodeForVirtualViewId()");
455         }
456 
457         final int actions = node.getActions();
458         if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) {
459             throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in "
460                     + "populateNodeForVirtualViewId()");
461         }
462         if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) {
463             throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in "
464                     + "populateNodeForVirtualViewId()");
465         }
466 
467         // Don't allow the client to override these properties.
468         node.setPackageName(mView.getContext().getPackageName());
469         node.setSource(mView, virtualViewId);
470         node.setParent(mView);
471 
472         // Manage internal accessibility focus state.
473         if (mFocusedVirtualViewId == virtualViewId) {
474             node.setAccessibilityFocused(true);
475             node.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
476         } else {
477             node.setAccessibilityFocused(false);
478             node.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
479         }
480 
481         // Set the visibility based on the parent bound.
482         if (intersectVisibleToUser(tempParentRect)) {
483             node.setVisibleToUser(true);
484             node.setBoundsInParent(tempParentRect);
485         }
486 
487         // Calculate screen-relative bound.
488         mView.getLocationOnScreen(tempGlobalRect);
489         final int offsetX = tempGlobalRect[0];
490         final int offsetY = tempGlobalRect[1];
491         tempScreenRect.set(tempParentRect);
492         tempScreenRect.offset(offsetX, offsetY);
493         node.setBoundsInScreen(tempScreenRect);
494 
495         return node;
496     }
497 
ensureTempRects()498     private void ensureTempRects() {
499         mTempGlobalRect = new int[2];
500         mTempParentRect = new Rect();
501         mTempScreenRect = new Rect();
502     }
503 
performAction(int virtualViewId, int action, Bundle arguments)504     private boolean performAction(int virtualViewId, int action, Bundle arguments) {
505         switch (virtualViewId) {
506             case HOST_ID:
507                 return performActionForHost(action, arguments);
508             default:
509                 return performActionForChild(virtualViewId, action, arguments);
510         }
511     }
512 
performActionForHost(int action, Bundle arguments)513     private boolean performActionForHost(int action, Bundle arguments) {
514         return mView.performAccessibilityAction(action, arguments);
515     }
516 
performActionForChild(int virtualViewId, int action, Bundle arguments)517     private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) {
518         switch (action) {
519             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
520             case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
521                 return manageFocusForChild(virtualViewId, action);
522             default:
523                 return onPerformActionForVirtualView(virtualViewId, action, arguments);
524         }
525     }
526 
manageFocusForChild(int virtualViewId, int action)527     private boolean manageFocusForChild(int virtualViewId, int action) {
528         switch (action) {
529             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
530                 return requestAccessibilityFocus(virtualViewId);
531             case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
532                 return clearAccessibilityFocus(virtualViewId);
533             default:
534                 return false;
535         }
536     }
537 
538     /**
539      * Computes whether the specified {@link Rect} intersects with the visible
540      * portion of its parent {@link View}. Modifies {@code localRect} to contain
541      * only the visible portion.
542      *
543      * @param localRect A rectangle in local (parent) coordinates.
544      * @return Whether the specified {@link Rect} is visible on the screen.
545      */
intersectVisibleToUser(Rect localRect)546     private boolean intersectVisibleToUser(Rect localRect) {
547         // Missing or empty bounds mean this view is not visible.
548         if ((localRect == null) || localRect.isEmpty()) {
549             return false;
550         }
551 
552         // Attached to invisible window means this view is not visible.
553         if (mView.getWindowVisibility() != View.VISIBLE) {
554             return false;
555         }
556 
557         // An invisible predecessor means that this view is not visible.
558         ViewParent viewParent = mView.getParent();
559         while (viewParent instanceof View) {
560             final View view = (View) viewParent;
561             if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) {
562                 return false;
563             }
564             viewParent = view.getParent();
565         }
566 
567         // A null parent implies the view is not visible.
568         if (viewParent == null) {
569             return false;
570         }
571 
572         // If no portion of the parent is visible, this view is not visible.
573         if (mTempVisibleRect == null) {
574             mTempVisibleRect = new Rect();
575         }
576         final Rect tempVisibleRect = mTempVisibleRect;
577         if (!mView.getLocalVisibleRect(tempVisibleRect)) {
578             return false;
579         }
580 
581         // Check if the view intersects the visible portion of the parent.
582         return localRect.intersect(tempVisibleRect);
583     }
584 
585     /**
586      * Returns whether this virtual view is accessibility focused.
587      *
588      * @return True if the view is accessibility focused.
589      */
isAccessibilityFocused(int virtualViewId)590     private boolean isAccessibilityFocused(int virtualViewId) {
591         return (mFocusedVirtualViewId == virtualViewId);
592     }
593 
594     /**
595      * Attempts to give accessibility focus to a virtual view.
596      * <p>
597      * A virtual view will not actually take focus if
598      * {@link AccessibilityManager#isEnabled()} returns false,
599      * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false,
600      * or the view already has accessibility focus.
601      *
602      * @param virtualViewId The id of the virtual view on which to place
603      *            accessibility focus.
604      * @return Whether this virtual view actually took accessibility focus.
605      */
requestAccessibilityFocus(int virtualViewId)606     private boolean requestAccessibilityFocus(int virtualViewId) {
607         final AccessibilityManager accessibilityManager =
608                 (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
609 
610         if (!mManager.isEnabled()
611                 || !accessibilityManager.isTouchExplorationEnabled()) {
612             return false;
613         }
614         // TODO: Check virtual view visibility.
615         if (!isAccessibilityFocused(virtualViewId)) {
616             // Clear focus from the previously focused view, if applicable.
617             if (mFocusedVirtualViewId != INVALID_ID) {
618                 sendEventForVirtualView(mFocusedVirtualViewId,
619                         AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
620             }
621 
622             // Set focus on the new view.
623             mFocusedVirtualViewId = virtualViewId;
624 
625             // TODO: Only invalidate virtual view bounds.
626             mView.invalidate();
627             sendEventForVirtualView(virtualViewId,
628                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
629             return true;
630         }
631         return false;
632     }
633 
634     /**
635      * Attempts to clear accessibility focus from a virtual view.
636      *
637      * @param virtualViewId The id of the virtual view from which to clear
638      *            accessibility focus.
639      * @return Whether this virtual view actually cleared accessibility focus.
640      */
clearAccessibilityFocus(int virtualViewId)641     private boolean clearAccessibilityFocus(int virtualViewId) {
642         if (isAccessibilityFocused(virtualViewId)) {
643             mFocusedVirtualViewId = INVALID_ID;
644             mView.invalidate();
645             sendEventForVirtualView(virtualViewId,
646                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
647             return true;
648         }
649         return false;
650     }
651 
652     /**
653      * Provides a mapping between view-relative coordinates and logical
654      * items.
655      *
656      * @param x The view-relative x coordinate
657      * @param y The view-relative y coordinate
658      * @return virtual view identifier for the logical item under
659      *         coordinates (x,y)
660      */
getVirtualViewAt(float x, float y)661     protected abstract int getVirtualViewAt(float x, float y);
662 
663     /**
664      * Populates a list with the view's visible items. The ordering of items
665      * within {@code virtualViewIds} specifies order of accessibility focus
666      * traversal.
667      *
668      * @param virtualViewIds The list to populate with visible items
669      */
getVisibleVirtualViews(IntArray virtualViewIds)670     protected abstract void getVisibleVirtualViews(IntArray virtualViewIds);
671 
672     /**
673      * Populates an {@link AccessibilityEvent} with information about the
674      * specified item.
675      * <p>
676      * Implementations <b>must</b> populate the following required fields:
677      * <ul>
678      * <li>event text, see {@link AccessibilityEvent#getText} or
679      * {@link AccessibilityEvent#setContentDescription}
680      * </ul>
681      * <p>
682      * The helper class automatically populates the following fields with
683      * default values, but implementations may optionally override them:
684      * <ul>
685      * <li>item class name, set to android.view.View, see
686      * {@link AccessibilityEvent#setClassName}
687      * </ul>
688      * <p>
689      * The following required fields are automatically populated by the
690      * helper class and may not be overridden:
691      * <ul>
692      * <li>package name, set to the package of the host view's
693      * {@link Context}, see {@link AccessibilityEvent#setPackageName}
694      * <li>event source, set to the host view and virtual view identifier,
695      * see {@link android.view.accessibility.AccessibilityRecord#setSource(View, int)}
696      * </ul>
697      *
698      * @param virtualViewId The virtual view id for the item for which to
699      *            populate the event
700      * @param event The event to populate
701      */
onPopulateEventForVirtualView( int virtualViewId, AccessibilityEvent event)702     protected abstract void onPopulateEventForVirtualView(
703             int virtualViewId, AccessibilityEvent event);
704 
705     /**
706      * Populates an {@link AccessibilityEvent} with information about the host
707      * view.
708      * <p>
709      * The default implementation is a no-op.
710      *
711      * @param event the event to populate with information about the host view
712      */
onPopulateEventForHost(AccessibilityEvent event)713     protected void onPopulateEventForHost(AccessibilityEvent event) {
714         // Default implementation is no-op.
715     }
716 
717     /**
718      * Populates an {@link AccessibilityNodeInfo} with information
719      * about the specified item.
720      * <p>
721      * Implementations <b>must</b> populate the following required fields:
722      * <ul>
723      * <li>event text, see {@link AccessibilityNodeInfo#setText} or
724      * {@link AccessibilityNodeInfo#setContentDescription}
725      * <li>bounds in parent coordinates, see
726      * {@link AccessibilityNodeInfo#setBoundsInParent}
727      * </ul>
728      * <p>
729      * The helper class automatically populates the following fields with
730      * default values, but implementations may optionally override them:
731      * <ul>
732      * <li>enabled state, set to true, see
733      * {@link AccessibilityNodeInfo#setEnabled}
734      * <li>item class name, identical to the class name set by
735      * {@link #onPopulateEventForVirtualView}, see
736      * {@link AccessibilityNodeInfo#setClassName}
737      * </ul>
738      * <p>
739      * The following required fields are automatically populated by the
740      * helper class and may not be overridden:
741      * <ul>
742      * <li>package name, identical to the package name set by
743      * {@link #onPopulateEventForVirtualView}, see
744      * {@link AccessibilityNodeInfo#setPackageName}
745      * <li>node source, identical to the event source set in
746      * {@link #onPopulateEventForVirtualView}, see
747      * {@link AccessibilityNodeInfo#setSource(View, int)}
748      * <li>parent view, set to the host view, see
749      * {@link AccessibilityNodeInfo#setParent(View)}
750      * <li>visibility, computed based on parent-relative bounds, see
751      * {@link AccessibilityNodeInfo#setVisibleToUser}
752      * <li>accessibility focus, computed based on internal helper state, see
753      * {@link AccessibilityNodeInfo#setAccessibilityFocused}
754      * <li>bounds in screen coordinates, computed based on host view bounds,
755      * see {@link AccessibilityNodeInfo#setBoundsInScreen}
756      * </ul>
757      * <p>
758      * Additionally, the helper class automatically handles accessibility
759      * focus management by adding the appropriate
760      * {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} or
761      * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
762      * action. Implementations must <b>never</b> manually add these actions.
763      * <p>
764      * The helper class also automatically modifies parent- and
765      * screen-relative bounds to reflect the portion of the item visible
766      * within its parent.
767      *
768      * @param virtualViewId The virtual view identifier of the item for
769      *            which to populate the node
770      * @param node The node to populate
771      */
onPopulateNodeForVirtualView( int virtualViewId, AccessibilityNodeInfo node)772     protected abstract void onPopulateNodeForVirtualView(
773             int virtualViewId, AccessibilityNodeInfo node);
774 
775     /**
776      * Populates an {@link AccessibilityNodeInfo} with information about the
777      * host view.
778      * <p>
779      * The default implementation is a no-op.
780      *
781      * @param node the node to populate with information about the host view
782      */
onPopulateNodeForHost(AccessibilityNodeInfo node)783     protected void onPopulateNodeForHost(AccessibilityNodeInfo node) {
784         // Default implementation is no-op.
785     }
786 
787     /**
788      * Performs the specified accessibility action on the item associated
789      * with the virtual view identifier. See
790      * {@link AccessibilityNodeInfo#performAction(int, Bundle)} for
791      * more information.
792      * <p>
793      * Implementations <b>must</b> handle any actions added manually in
794      * {@link #onPopulateNodeForVirtualView}.
795      * <p>
796      * The helper class automatically handles focus management resulting
797      * from {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS}
798      * and
799      * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
800      * actions.
801      *
802      * @param virtualViewId The virtual view identifier of the item on which
803      *            to perform the action
804      * @param action The accessibility action to perform
805      * @param arguments (Optional) A bundle with additional arguments, or
806      *            null
807      * @return true if the action was performed
808      */
onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments)809     protected abstract boolean onPerformActionForVirtualView(
810             int virtualViewId, int action, Bundle arguments);
811 
812     /**
813      * Exposes a virtual view hierarchy to the accessibility framework. Only
814      * used in API 16+.
815      */
816     private class ExploreByTouchNodeProvider extends AccessibilityNodeProvider {
817         @Override
createAccessibilityNodeInfo(int virtualViewId)818         public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
819             return ExploreByTouchHelper.this.createNode(virtualViewId);
820         }
821 
822         @Override
performAction(int virtualViewId, int action, Bundle arguments)823         public boolean performAction(int virtualViewId, int action, Bundle arguments) {
824             return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments);
825         }
826     }
827 }
828