• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.example.android.apis.accessibility;
18 
19 import com.example.android.apis.R;
20 
21 import android.app.Activity;
22 import android.app.Service;
23 import android.content.Context;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.Paint;
27 import android.graphics.Rect;
28 import android.os.Bundle;
29 import android.text.TextUtils;
30 import android.util.AttributeSet;
31 import android.view.MotionEvent;
32 import android.view.View;
33 import android.view.accessibility.AccessibilityEvent;
34 import android.view.accessibility.AccessibilityManager;
35 import android.view.accessibility.AccessibilityNodeInfo;
36 import android.view.accessibility.AccessibilityNodeProvider;
37 
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 
42 /**
43  * This sample demonstrates how a View can expose a virtual view sub-tree
44  * rooted at it. A virtual sub-tree is composed of imaginary Views
45  * that are reported as a part of the view hierarchy for accessibility
46  * purposes. This enables custom views that draw complex content to report
47  * them selves as a tree of virtual views, thus conveying their logical
48  * structure.
49  * <p>
50  * For example, a View may draw a monthly calendar as a grid of days while
51  * each such day may contains some events. From a perspective of the View
52  * hierarchy the calendar is composed of a single View but an accessibility
53  * service would benefit of traversing the logical structure of the calendar
54  * by examining each day and each event on that day.
55  * </p>
56  */
57 public class AccessibilityNodeProviderActivity extends Activity {
58     /** Called when the activity is first created. */
59     @Override
onCreate(Bundle savedInstanceState)60     public void onCreate(Bundle savedInstanceState) {
61         super.onCreate(savedInstanceState);
62         setContentView(R.layout.accessibility_node_provider);
63     }
64 
65    /**
66     * This class presents a View that is composed of three virtual children
67     * each of which is drawn with a different color and represents a region
68     * of the View that has different semantics compared to other such regions.
69     * While the virtual view tree exposed by this class is one level deep
70     * for simplicity, there is no bound on the complexity of that virtual
71     * sub-tree.
72     */
73     public static class VirtualSubtreeRootView extends View {
74 
75         /** Paint object for drawing the virtual sub-tree */
76         private final Paint mPaint = new Paint();
77 
78         /** Temporary rectangle to minimize object creation. */
79         private final Rect mTempRect = new Rect();
80 
81         /** Handle to the system accessibility service. */
82         private final AccessibilityManager mAccessibilityManager;
83 
84         /** The virtual children of this View. */
85         private final List<VirtualView> mChildren = new ArrayList<VirtualView>();
86 
87         /** The instance of the node provider for the virtual tree - lazily instantiated. */
88         private AccessibilityNodeProvider mAccessibilityNodeProvider;
89 
90         /** The last hovered child used for event dispatching. */
91         private VirtualView mLastHoveredChild;
92 
VirtualSubtreeRootView(Context context, AttributeSet attrs)93         public VirtualSubtreeRootView(Context context, AttributeSet attrs) {
94             super(context, attrs);
95             mAccessibilityManager = (AccessibilityManager) context.getSystemService(
96                     Service.ACCESSIBILITY_SERVICE);
97             createVirtualChildren();
98         }
99 
100         /**
101          * {@inheritDoc}
102          */
103         @Override
getAccessibilityNodeProvider()104         public AccessibilityNodeProvider getAccessibilityNodeProvider() {
105             // Instantiate the provide only when requested. Since the system
106             // will call this method multiple times it is a good practice to
107             // cache the provider instance.
108             if (mAccessibilityNodeProvider == null) {
109                 mAccessibilityNodeProvider = new VirtualDescendantsProvider();
110             }
111             return mAccessibilityNodeProvider;
112         }
113 
114         /**
115          * {@inheritDoc}
116          */
117         @Override
dispatchHoverEvent(MotionEvent event)118         public boolean dispatchHoverEvent(MotionEvent event) {
119             // This implementation assumes that the virtual children
120             // cannot overlap and are always visible. Do NOT use this
121             // code as a reference of how to implement hover event
122             // dispatch. Instead, refer to ViewGroup#dispatchHoverEvent.
123             boolean handled = false;
124             List<VirtualView> children = mChildren;
125             final int childCount = children.size();
126             for (int i = 0; i < childCount; i++) {
127                 VirtualView child = children.get(i);
128                 Rect childBounds = child.mBounds;
129                 final int childCoordsX = (int) event.getX() + getScrollX();
130                 final int childCoordsY = (int) event.getY() + getScrollY();
131                 if (!childBounds.contains(childCoordsX, childCoordsY)) {
132                     continue;
133                 }
134                 final int action = event.getAction();
135                 switch (action) {
136                     case MotionEvent.ACTION_HOVER_ENTER: {
137                         mLastHoveredChild = child;
138                         handled |= onHoverVirtualView(child, event);
139                         event.setAction(action);
140                     } break;
141                     case MotionEvent.ACTION_HOVER_MOVE: {
142                         if (child == mLastHoveredChild) {
143                             handled |= onHoverVirtualView(child, event);
144                             event.setAction(action);
145                         } else {
146                             MotionEvent eventNoHistory = event.getHistorySize() > 0
147                                 ? MotionEvent.obtainNoHistory(event) : event;
148                             eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
149                             onHoverVirtualView(mLastHoveredChild, eventNoHistory);
150                             eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
151                             onHoverVirtualView(child, eventNoHistory);
152                             mLastHoveredChild = child;
153                             eventNoHistory.setAction(MotionEvent.ACTION_HOVER_MOVE);
154                             handled |= onHoverVirtualView(child, eventNoHistory);
155                             if (eventNoHistory != event) {
156                                 eventNoHistory.recycle();
157                             } else {
158                                 event.setAction(action);
159                             }
160                         }
161                     } break;
162                     case MotionEvent.ACTION_HOVER_EXIT: {
163                         mLastHoveredChild = null;
164                         handled |= onHoverVirtualView(child, event);
165                         event.setAction(action);
166                     } break;
167                 }
168             }
169             if (!handled) {
170                 handled |= onHoverEvent(event);
171             }
172             return handled;
173         }
174 
175         /**
176          * {@inheritDoc}
177          */
178         @Override
onLayout(boolean changed, int left, int top, int right, int bottom)179         protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
180             // The virtual children are ordered horizontally next to
181             // each other and take the entire space of this View.
182             int offsetX = 0;
183             List<VirtualView> children = mChildren;
184             final int childCount = children.size();
185             for (int i = 0; i < childCount; i++) {
186                 VirtualView child = children.get(i);
187                 Rect childBounds = child.mBounds;
188                 childBounds.set(offsetX, 0, offsetX + childBounds.width(), childBounds.height());
189                 offsetX += childBounds.width();
190             }
191         }
192 
193         /**
194          * {@inheritDoc}
195          */
196         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)197         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
198             // The virtual children are ordered horizontally next to
199             // each other and take the entire space of this View.
200             int width = 0;
201             int height = 0;
202             List<VirtualView> children = mChildren;
203             final int childCount = children.size();
204             for (int i = 0; i < childCount; i++) {
205                 VirtualView child = children.get(i);
206                 width += child.mBounds.width();
207                 height = Math.max(height, child.mBounds.height());
208             }
209             setMeasuredDimension(width, height);
210         }
211 
212         /**
213          * {@inheritDoc}
214          */
215         @Override
onDraw(Canvas canvas)216         protected void onDraw(Canvas canvas) {
217             // Draw the virtual children with the reusable Paint object
218             // and with the bounds and color which are child specific.
219             Rect drawingRect = mTempRect;
220             List<VirtualView> children = mChildren;
221             final int childCount = children.size();
222             for (int i = 0; i < childCount; i++) {
223                 VirtualView child = children.get(i);
224                 drawingRect.set(child.mBounds);
225                 mPaint.setColor(child.mColor);
226                 mPaint.setAlpha(child.mAlpha);
227                 canvas.drawRect(drawingRect, mPaint);
228             }
229         }
230 
231         /**
232          * Creates the virtual children of this View.
233          */
createVirtualChildren()234         private void createVirtualChildren() {
235             // The virtual portion of the tree is one level deep. Note
236             // that implementations can use any way of representing and
237             // drawing virtual view.
238             VirtualView firstChild = new VirtualView(0, new Rect(0, 0, 150, 150), Color.RED,
239                     "Virtual view 1");
240             mChildren.add(firstChild);
241             VirtualView secondChild = new VirtualView(1, new Rect(0, 0, 150, 150), Color.GREEN,
242                     "Virtual view 2");
243             mChildren.add(secondChild);
244             VirtualView thirdChild = new VirtualView(2, new Rect(0, 0, 150, 150), Color.BLUE,
245                     "Virtual view 3");
246             mChildren.add(thirdChild);
247         }
248 
249         /**
250          * Set the selected state of a virtual view.
251          *
252          * @param virtualView The virtual view whose selected state to set.
253          * @param selected Whether the virtual view is selected.
254          */
setVirtualViewSelected(VirtualView virtualView, boolean selected)255         private void setVirtualViewSelected(VirtualView virtualView, boolean selected) {
256             virtualView.mAlpha = selected ? VirtualView.ALPHA_SELECTED : VirtualView.ALPHA_NOT_SELECTED;
257         }
258 
259         /**
260          * Handle a hover over a virtual view.
261          *
262          * @param virtualView The virtual view over which is hovered.
263          * @param event The event to dispatch.
264          * @return Whether the event was handled.
265          */
onHoverVirtualView(VirtualView virtualView, MotionEvent event)266         private boolean onHoverVirtualView(VirtualView virtualView, MotionEvent event) {
267             // The implementation of hover event dispatch can be implemented
268             // in any way that is found suitable. However, each virtual View
269             // should fire a corresponding accessibility event whose source
270             // is that virtual view. Accessibility services get the event source
271             // as the entry point of the APIs for querying the window content.
272             final int action = event.getAction();
273             switch (action) {
274                 case MotionEvent.ACTION_HOVER_ENTER: {
275                     sendAccessibilityEventForVirtualView(virtualView,
276                             AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
277                 } break;
278                 case MotionEvent.ACTION_HOVER_EXIT: {
279                     sendAccessibilityEventForVirtualView(virtualView,
280                             AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
281                 } break;
282             }
283             return true;
284         }
285 
286         /**
287          * Sends a properly initialized accessibility event for a virtual view..
288          *
289          * @param virtualView The virtual view.
290          * @param eventType The type of the event to send.
291          */
sendAccessibilityEventForVirtualView(VirtualView virtualView, int eventType)292         private void sendAccessibilityEventForVirtualView(VirtualView virtualView, int eventType) {
293             // If touch exploration, i.e. the user gets feedback while touching
294             // the screen, is enabled we fire accessibility events.
295             if (mAccessibilityManager.isTouchExplorationEnabled()) {
296                 AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
297                 event.setPackageName(getContext().getPackageName());
298                 event.setClassName(virtualView.getClass().getName());
299                 event.setSource(VirtualSubtreeRootView.this, virtualView.mId);
300                 event.getText().add(virtualView.mText);
301                 getParent().requestSendAccessibilityEvent(VirtualSubtreeRootView.this, event);
302             }
303         }
304 
305         /**
306          * Finds a virtual view given its id.
307          *
308          * @param id The virtual view id.
309          * @return The found virtual view.
310          */
findVirtualViewById(int id)311         private VirtualView findVirtualViewById(int id) {
312             List<VirtualView> children = mChildren;
313             final int childCount = children.size();
314             for (int i = 0; i < childCount; i++) {
315                 VirtualView child = children.get(i);
316                 if (child.mId == id) {
317                     return child;
318                 }
319             }
320             return null;
321         }
322 
323         /**
324          * Represents a virtual View.
325          */
326         private class VirtualView {
327             public static final int ALPHA_SELECTED = 255;
328             public static final int ALPHA_NOT_SELECTED = 127;
329 
330             public final int mId;
331             public final int mColor;
332             public final Rect mBounds;
333             public final String mText;
334             public int mAlpha;
335 
VirtualView(int id, Rect bounds, int color, String text)336             public VirtualView(int id, Rect bounds, int color, String text) {
337                 mId = id;
338                 mColor = color;
339                 mBounds = bounds;
340                 mText = text;
341                 mAlpha = ALPHA_NOT_SELECTED;
342             }
343         }
344 
345         /**
346          * This is the provider that exposes the virtual View tree to accessibility
347          * services. From the perspective of an accessibility service the
348          * {@link AccessibilityNodeInfo}s it receives while exploring the sub-tree
349          * rooted at this View will be the same as the ones it received while
350          * exploring a View containing a sub-tree composed of real Views.
351          */
352         private class VirtualDescendantsProvider extends AccessibilityNodeProvider {
353 
354             /**
355              * {@inheritDoc}
356              */
357             @Override
createAccessibilityNodeInfo(int virtualViewId)358             public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
359                 AccessibilityNodeInfo info = null;
360                 if (virtualViewId == View.NO_ID) {
361                     // We are requested to create an AccessibilityNodeInfo describing
362                     // this View, i.e. the root of the virtual sub-tree. Note that the
363                     // host View has an AccessibilityNodeProvider which means that this
364                     // provider is responsible for creating the node info for that root.
365                     info = AccessibilityNodeInfo.obtain(VirtualSubtreeRootView.this);
366                     onInitializeAccessibilityNodeInfo(info);
367                     // Add the virtual children of the root View.
368                     List<VirtualView> children = mChildren;
369                     final int childCount = children.size();
370                     for (int i = 0; i < childCount; i++) {
371                         VirtualView child = children.get(i);
372                         info.addChild(VirtualSubtreeRootView.this, child.mId);
373                     }
374                 } else {
375                     // Find the view that corresponds to the given id.
376                     VirtualView virtualView = findVirtualViewById(virtualViewId);
377                     if (virtualView == null) {
378                         return null;
379                     }
380                     // Obtain and initialize an AccessibilityNodeInfo with
381                     // information about the virtual view.
382                     info = AccessibilityNodeInfo.obtain();
383                     info.addAction(AccessibilityNodeInfo.ACTION_SELECT);
384                     info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);
385                     info.setPackageName(getContext().getPackageName());
386                     info.setClassName(virtualView.getClass().getName());
387                     info.setSource(VirtualSubtreeRootView.this, virtualViewId);
388                     info.setBoundsInParent(virtualView.mBounds);
389                     info.setParent(VirtualSubtreeRootView.this);
390                     info.setText(virtualView.mText);
391                 }
392                 return info;
393             }
394 
395             /**
396              * {@inheritDoc}
397              */
398             @Override
findAccessibilityNodeInfosByText(String searched, int virtualViewId)399             public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched,
400                     int virtualViewId) {
401                 if (TextUtils.isEmpty(searched)) {
402                     return Collections.emptyList();
403                 }
404                 String searchedLowerCase = searched.toLowerCase();
405                 List<AccessibilityNodeInfo> result = null;
406                 if (virtualViewId == View.NO_ID) {
407                     // If the search is from the root, i.e. this View, go over the virtual
408                     // children and look for ones that contain the searched string since
409                     // this View does not contain text itself.
410                     List<VirtualView> children = mChildren;
411                     final int childCount = children.size();
412                     for (int i = 0; i < childCount; i++) {
413                         VirtualView child = children.get(i);
414                         String textToLowerCase = child.mText.toLowerCase();
415                         if (textToLowerCase.contains(searchedLowerCase)) {
416                             if (result == null) {
417                                 result = new ArrayList<AccessibilityNodeInfo>();
418                             }
419                             result.add(createAccessibilityNodeInfo(child.mId));
420                         }
421                     }
422                 } else {
423                     // If the search is from a virtual view, find the view. Since the tree
424                     // is one level deep we add a node info for the child to the result if
425                     // the child contains the searched text.
426                     VirtualView virtualView = findVirtualViewById(virtualViewId);
427                     if (virtualView != null) {
428                         String textToLowerCase = virtualView.mText.toLowerCase();
429                         if (textToLowerCase.contains(searchedLowerCase)) {
430                             result = new ArrayList<AccessibilityNodeInfo>();
431                             result.add(createAccessibilityNodeInfo(virtualViewId));
432                         }
433                     }
434                 }
435                 if (result == null) {
436                     return Collections.emptyList();
437                 }
438                 return result;
439             }
440 
441             /**
442              * {@inheritDoc}
443              */
444             @Override
performAction(int virtualViewId, int action, Bundle arguments)445             public boolean performAction(int virtualViewId, int action, Bundle arguments) {
446                 if (virtualViewId == View.NO_ID) {
447                     // Perform the action on the host View.
448                     switch (action) {
449                         case AccessibilityNodeInfo.ACTION_SELECT:
450                             if (!isSelected()) {
451                                 setSelected(true);
452                                 return isSelected();
453                             }
454                             break;
455                         case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION:
456                             if (isSelected()) {
457                                 setSelected(false);
458                                 return !isSelected();
459                             }
460                             break;
461                     }
462                 } else {
463                     // Find the view that corresponds to the given id.
464                     VirtualView child = findVirtualViewById(virtualViewId);
465                     if (child == null) {
466                         return false;
467                     }
468                     // Perform the action on a virtual view.
469                     switch (action) {
470                         case AccessibilityNodeInfo.ACTION_SELECT:
471                             setVirtualViewSelected(child, true);
472                             invalidate();
473                             return true;
474                         case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION:
475                             setVirtualViewSelected(child, false);
476                             invalidate();
477                             return true;
478                     }
479                 }
480                 return false;
481             }
482         }
483     }
484 }
485