• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.googlecode.eyesfree.utils;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.os.Bundle;
22 import android.support.v4.view.AccessibilityDelegateCompat;
23 import android.support.v4.view.ViewCompat;
24 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
25 import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
26 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
27 import android.text.TextUtils;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.accessibility.AccessibilityEvent;
32 import android.view.accessibility.AccessibilityManager;
33 
34 import java.util.LinkedList;
35 import java.util.List;
36 
37 public abstract class TouchExplorationHelper<T> extends AccessibilityNodeProviderCompat
38         implements View.OnHoverListener {
39     /** Virtual node identifier value for invalid nodes. */
40     public static final int INVALID_ID = Integer.MIN_VALUE;
41 
42     private final Rect mTempScreenRect = new Rect();
43     private final Rect mTempParentRect = new Rect();
44     private final Rect mTempVisibleRect = new Rect();
45     private final int[] mTempGlobalRect = new int[2];
46 
47     private final AccessibilityManager mManager;
48 
49     private View mParentView;
50     private int mFocusedItemId = INVALID_ID;
51     private T mCurrentItem = null;
52 
53     /**
54      * Constructs a new touch exploration helper.
55      *
56      * @param context The parent context.
57      */
TouchExplorationHelper(Context context, View parentView)58     public TouchExplorationHelper(Context context, View parentView) {
59         mManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
60         mParentView = parentView;
61     }
62 
63     /**
64      * @return The current accessibility focused item, or {@code null} if no
65      *         item is focused.
66      */
getFocusedItem()67     public T getFocusedItem() {
68         return getItemForId(mFocusedItemId);
69     }
70 
71     /**
72      * Clears the current accessibility focused item.
73      */
clearFocusedItem()74     public void clearFocusedItem() {
75         final int itemId = mFocusedItemId;
76         if (itemId == INVALID_ID) {
77             return;
78         }
79 
80         performAction(itemId, AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
81     }
82 
83     /**
84      * Requests accessibility focus be placed on the specified item.
85      *
86      * @param item The item to place focus on.
87      */
setFocusedItem(T item)88     public void setFocusedItem(T item) {
89         final int itemId = getIdForItem(item);
90         if (itemId == INVALID_ID) {
91             return;
92         }
93 
94         performAction(itemId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
95     }
96 
97     /**
98      * Invalidates cached information about the parent view.
99      * <p>
100      * You <b>must</b> call this method after adding or removing items from the
101      * parent view.
102      * </p>
103      */
invalidateParent()104     public void invalidateParent() {
105         mParentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
106     }
107 
108     /**
109      * Invalidates cached information for a particular item.
110      * <p>
111      * You <b>must</b> call this method when any of the properties set in
112      * {@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)} have
113      * changed.
114      * </p>
115      *
116      * @param item
117      */
invalidateItem(T item)118     public void invalidateItem(T item) {
119         sendEventForItem(item, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
120     }
121 
122     /**
123      * Populates an event of the specified type with information about an item
124      * and attempts to send it up through the view hierarchy.
125      *
126      * @param item The item for which to send an event.
127      * @param eventType The type of event to send.
128      * @return {@code true} if the event was sent successfully.
129      */
sendEventForItem(T item, int eventType)130     public boolean sendEventForItem(T item, int eventType) {
131         if (!mManager.isEnabled()) {
132             return false;
133         }
134 
135         final AccessibilityEvent event = getEventForItem(item, eventType);
136         final ViewGroup group = (ViewGroup) mParentView.getParent();
137 
138         return group.requestSendAccessibilityEvent(mParentView, event);
139     }
140 
141     @Override
createAccessibilityNodeInfo(int virtualViewId)142     public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
143         if (virtualViewId == View.NO_ID) {
144             return getNodeForParent();
145         }
146 
147         final T item = getItemForId(virtualViewId);
148         if (item == null) {
149             return null;
150         }
151 
152         final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain();
153         populateNodeForItemInternal(item, node);
154         return node;
155     }
156 
157     @Override
performAction(int virtualViewId, int action, Bundle arguments)158     public boolean performAction(int virtualViewId, int action, Bundle arguments) {
159         if (virtualViewId == View.NO_ID) {
160             return ViewCompat.performAccessibilityAction(mParentView, action, arguments);
161         }
162 
163         final T item = getItemForId(virtualViewId);
164         if (item == null) {
165             return false;
166         }
167 
168         boolean handled = false;
169 
170         switch (action) {
171             case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
172                 if (mFocusedItemId != virtualViewId) {
173                     mFocusedItemId = virtualViewId;
174                     sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
175                     handled = true;
176                 }
177                 break;
178             case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
179                 if (mFocusedItemId == virtualViewId) {
180                     mFocusedItemId = INVALID_ID;
181                     sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
182                     handled = true;
183                 }
184                 break;
185         }
186 
187         handled |= performActionForItem(item, action, arguments);
188 
189         return handled;
190     }
191 
192     @Override
onHover(View view, MotionEvent event)193     public boolean onHover(View view, MotionEvent event) {
194         if (!mManager.isTouchExplorationEnabled()) {
195             return false;
196         }
197 
198         switch (event.getAction()) {
199             case MotionEvent.ACTION_HOVER_ENTER:
200             case MotionEvent.ACTION_HOVER_MOVE:
201                 final T item = getItemAt(event.getX(), event.getY());
202                 setCurrentItem(item);
203                 return true;
204             case MotionEvent.ACTION_HOVER_EXIT:
205                 setCurrentItem(null);
206                 return true;
207         }
208 
209         return false;
210     }
211 
setCurrentItem(T item)212     private void setCurrentItem(T item) {
213         if (mCurrentItem == item) {
214             return;
215         }
216 
217         if (mCurrentItem != null) {
218             sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
219         }
220 
221         mCurrentItem = item;
222 
223         if (mCurrentItem != null) {
224             sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
225         }
226     }
227 
getEventForItem(T item, int eventType)228     private AccessibilityEvent getEventForItem(T item, int eventType) {
229         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
230         final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
231         final int virtualDescendantId = getIdForItem(item);
232 
233         // Ensure the client has good defaults.
234         event.setEnabled(true);
235 
236         // Allow the client to populate the event.
237         populateEventForItem(item, event);
238 
239         if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) {
240             throw new RuntimeException(
241                     "You must add text or a content description in populateEventForItem()");
242         }
243 
244         // Don't allow the client to override these properties.
245         event.setClassName(item.getClass().getName());
246         event.setPackageName(mParentView.getContext().getPackageName());
247         record.setSource(mParentView, virtualDescendantId);
248 
249         return event;
250     }
251 
getNodeForParent()252     private AccessibilityNodeInfoCompat getNodeForParent() {
253         final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(mParentView);
254         ViewCompat.onInitializeAccessibilityNodeInfo(mParentView, info);
255 
256         final LinkedList<T> items = new LinkedList<T>();
257         getVisibleItems(items);
258 
259         for (T item : items) {
260             final int virtualDescendantId = getIdForItem(item);
261             info.addChild(mParentView, virtualDescendantId);
262         }
263 
264         return info;
265     }
266 
populateNodeForItemInternal( T item, AccessibilityNodeInfoCompat node)267     private AccessibilityNodeInfoCompat populateNodeForItemInternal(
268             T item, AccessibilityNodeInfoCompat node) {
269         final int virtualDescendantId = getIdForItem(item);
270 
271         // Ensure the client has good defaults.
272         node.setEnabled(true);
273 
274         // Allow the client to populate the node.
275         populateNodeForItem(item, node);
276 
277         if (TextUtils.isEmpty(node.getText()) && TextUtils.isEmpty(node.getContentDescription())) {
278             throw new RuntimeException(
279                     "You must add text or a content description in populateNodeForItem()");
280         }
281 
282         // Don't allow the client to override these properties.
283         node.setPackageName(mParentView.getContext().getPackageName());
284         node.setClassName(item.getClass().getName());
285         node.setParent(mParentView);
286         node.setSource(mParentView, virtualDescendantId);
287 
288         if (mFocusedItemId == virtualDescendantId) {
289             node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
290         } else {
291             node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
292         }
293 
294         node.getBoundsInParent(mTempParentRect);
295         if (mTempParentRect.isEmpty()) {
296             throw new RuntimeException("You must set parent bounds in populateNodeForItem()");
297         }
298 
299         // Set the visibility based on the parent bound.
300         if (intersectVisibleToUser(mTempParentRect)) {
301             node.setVisibleToUser(true);
302             node.setBoundsInParent(mTempParentRect);
303         }
304 
305         // Calculate screen-relative bound.
306         mParentView.getLocationOnScreen(mTempGlobalRect);
307         final int offsetX = mTempGlobalRect[0];
308         final int offsetY = mTempGlobalRect[1];
309         mTempScreenRect.set(mTempParentRect);
310         mTempScreenRect.offset(offsetX, offsetY);
311         node.setBoundsInScreen(mTempScreenRect);
312 
313         return node;
314     }
315 
316     /**
317      * Computes whether the specified {@link Rect} intersects with the visible
318      * portion of its parent {@link View}. Modifies {@code localRect} to
319      * contain only the visible portion.
320      *
321      * @param localRect A rectangle in local (parent) coordinates.
322      * @return Whether the specified {@link Rect} is visible on the screen.
323      */
intersectVisibleToUser(Rect localRect)324     private boolean intersectVisibleToUser(Rect localRect) {
325         // Missing or empty bounds mean this view is not visible.
326         if ((localRect == null) || localRect.isEmpty()) {
327             return false;
328         }
329 
330         // Attached to invisible window means this view is not visible.
331         if (mParentView.getWindowVisibility() != View.VISIBLE) {
332             return false;
333         }
334 
335         // An invisible predecessor or one with alpha zero means
336         // that this view is not visible to the user.
337         Object current = this;
338         while (current instanceof View) {
339             final View view = (View) current;
340             // We have attach info so this view is attached and there is no
341             // need to check whether we reach to ViewRootImpl on the way up.
342             if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) {
343                 return false;
344             }
345             current = view.getParent();
346         }
347 
348         // If no portion of the parent is visible, this view is not visible.
349         if (!mParentView.getLocalVisibleRect(mTempVisibleRect)) {
350             return false;
351         }
352 
353         // Check if the view intersects the visible portion of the parent.
354         return localRect.intersect(mTempVisibleRect);
355     }
356 
getAccessibilityDelegate()357     public AccessibilityDelegateCompat getAccessibilityDelegate() {
358         return mDelegate;
359     }
360 
361     private final AccessibilityDelegateCompat mDelegate = new AccessibilityDelegateCompat() {
362         @Override
363         public void onInitializeAccessibilityEvent(View view, AccessibilityEvent event) {
364             super.onInitializeAccessibilityEvent(view, event);
365             event.setClassName(view.getClass().getName());
366         }
367 
368         @Override
369         public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfoCompat info) {
370             super.onInitializeAccessibilityNodeInfo(view, info);
371             info.setClassName(view.getClass().getName());
372         }
373 
374         @Override
375         public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
376             return TouchExplorationHelper.this;
377         }
378     };
379 
380     /**
381      * Performs an accessibility action on the specified item. See
382      * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)}.
383      * <p>
384      * The helper class automatically handles focus management resulting from
385      * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} and
386      * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS}, so
387      * typically a developer only needs to handle actions added manually in the
388      * {{@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)}
389      * method.
390      * </p>
391      *
392      * @param item The item on which to perform the action.
393      * @param action The accessibility action to perform.
394      * @param arguments Arguments for the action, or optionally {@code null}.
395      * @return {@code true} if the action was performed successfully.
396      */
performActionForItem(T item, int action, Bundle arguments)397     protected abstract boolean performActionForItem(T item, int action, Bundle arguments);
398 
399     /**
400      * Populates an event with information about the specified item.
401      * <p>
402      * At a minimum, a developer must populate the event text by doing one of
403      * the following:
404      * <ul>
405      * <li>appending text to {@link AccessibilityEvent#getText()}</li>
406      * <li>populating a description with
407      * {@link AccessibilityEvent#setContentDescription(CharSequence)}</li>
408      * </ul>
409      * </p>
410      *
411      * @param item The item for which to populate the event.
412      * @param event The event to populate.
413      */
populateEventForItem(T item, AccessibilityEvent event)414     protected abstract void populateEventForItem(T item, AccessibilityEvent event);
415 
416     /**
417      * Populates a node with information about the specified item.
418      * <p>
419      * At a minimum, a developer must:
420      * <ul>
421      * <li>populate the event text using
422      * {@link AccessibilityNodeInfoCompat#setText(CharSequence)} or
423      * {@link AccessibilityNodeInfoCompat#setContentDescription(CharSequence)}
424      * </li>
425      * <li>set the item's parent-relative bounds using
426      * {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)}
427      * </ul>
428      *
429      * @param item The item for which to populate the node.
430      * @param node The node to populate.
431      */
populateNodeForItem(T item, AccessibilityNodeInfoCompat node)432     protected abstract void populateNodeForItem(T item, AccessibilityNodeInfoCompat node);
433 
434     /**
435      * Populates a list with the parent view's visible items.
436      * <p>
437      * The result of this method is cached until the developer calls
438      * {@link #invalidateParent()}.
439      * </p>
440      *
441      * @param items The list to populate with visible items.
442      */
getVisibleItems(List<T> items)443     protected abstract void getVisibleItems(List<T> items);
444 
445     /**
446      * Returns the item under the specified parent-relative coordinates.
447      *
448      * @param x The parent-relative x coordinate.
449      * @param y The parent-relative y coordinate.
450      * @return The item under coordinates (x,y).
451      */
getItemAt(float x, float y)452     protected abstract T getItemAt(float x, float y);
453 
454     /**
455      * Returns the unique identifier for an item. If the specified item does not
456      * exist, returns {@link #INVALID_ID}.
457      * <p>
458      * This result of this method must be consistent with
459      * {@link #getItemForId(int)}.
460      * </p>
461      *
462      * @param item The item whose identifier to return.
463      * @return A unique identifier, or {@link #INVALID_ID}.
464      */
getIdForItem(T item)465     protected abstract int getIdForItem(T item);
466 
467     /**
468      * Returns the item for a unique identifier. If the specified item does not
469      * exist, returns {@code null}.
470      *
471      * @param id The identifier for the item to return.
472      * @return An item, or {@code null}.
473      */
getItemForId(int id)474     protected abstract T getItemForId(int id);
475 }
476