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