Adding a code sample for using AccessibilityNodeProvider to report virtual Views.
Note: This is a sample and does *not* affect the system image, rather only the SDK.
bug:5508317
Change-Id: I62bbef4b2a4c2789ddfa128e94ae37246d244ac0
diff --git a/samples/ApiDemos/AndroidManifest.xml b/samples/ApiDemos/AndroidManifest.xml
index 2449e06..3ee5e5e 100644
--- a/samples/ApiDemos/AndroidManifest.xml
+++ b/samples/ApiDemos/AndroidManifest.xml
@@ -834,6 +834,16 @@
</intent-filter>
</activity>
+ <!-- Accessibility Samples -->
+ <activity android:name=".accessibility.AccessibilityNodeProviderActivity"
+ android:label="@string/accessibility_node_provider"
+ android:enabled="@bool/atLeastIceCreamSandwich">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.SAMPLE_CODE" />
+ </intent-filter>
+ </activity>
+
<!-- Application Updating Samples -->
<!-- BEGIN_INCLUDE(app_update_declaration) -->
diff --git a/samples/ApiDemos/_index.html b/samples/ApiDemos/_index.html
index 9466c95..ee230a9 100644
--- a/samples/ApiDemos/_index.html
+++ b/samples/ApiDemos/_index.html
@@ -36,6 +36,7 @@
<li><a href="src/com/example/android/apis/graphics/TouchPaint.html">Stylus and hover
support</a></li>
<li><a href="src/com/example/android/apis/view/Switches.html">Switch widget</a></li>
+<li><a href="src/com/example/android/apis/accessibility/AccessibilityNodeProviderActivity.html">Accessibility Node Provider</a></li>
</ul>
</div>
diff --git a/samples/ApiDemos/res/layout/accessibility_node_provider.xml b/samples/ApiDemos/res/layout/accessibility_node_provider.xml
new file mode 100644
index 0000000..cc10c9c
--- /dev/null
+++ b/samples/ApiDemos/res/layout/accessibility_node_provider.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="50dip"
+ android:text="@string/accessibility_node_provider_instructions">
+ </TextView>
+
+ <view
+ class="com.example.android.apis.accessibility.AccessibilityNodeProviderActivity$VirtualSubtreeRootView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" >
+ </view>
+
+</LinearLayout>
diff --git a/samples/ApiDemos/res/values/strings.xml b/samples/ApiDemos/res/values/strings.xml
index 5a7183c..9085500 100644
--- a/samples/ApiDemos/res/values/strings.xml
+++ b/samples/ApiDemos/res/values/strings.xml
@@ -1286,4 +1286,13 @@
<string name="dismiss">Dismiss</string>
<string name="share">Share</string>
+
+ <!-- ============================ -->
+ <!-- Accessibility examples strings -->
+ <!-- ============================ -->
+
+ <string name="accessibility_node_provider">Accessibility/Accessibility Node Provider</string>
+ <string name="accessibility_node_provider_instructions">Enable TalkBack and Explore-by-touch from accessibility
+ settings. Then touch the colored squares.</string>
+
</resources>
diff --git a/samples/ApiDemos/src/com/example/android/apis/accessibility/AccessibilityNodeProviderActivity.java b/samples/ApiDemos/src/com/example/android/apis/accessibility/AccessibilityNodeProviderActivity.java
new file mode 100644
index 0000000..16914c7
--- /dev/null
+++ b/samples/ApiDemos/src/com/example/android/apis/accessibility/AccessibilityNodeProviderActivity.java
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.apis.accessibility;
+
+import com.example.android.apis.R;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This sample demonstrates how a View can expose a virtual view sub-tree
+ * rooted at it. A virtual sub-tree is composed of imaginary Views
+ * that are reported as a part of the view hierarchy for accessibility
+ * purposes. This enables custom views that draw complex content to report
+ * them selves as a tree of virtual views, thus conveying their logical
+ * structure.
+ * <p>
+ * For example, a View may draw a monthly calendar as a grid of days while
+ * each such day may contains some events. From a perspective of the View
+ * hierarchy the calendar is composed of a single View but an accessibility
+ * service would benefit of traversing the logical structure of the calendar
+ * by examining each day and each event on that day.
+ * </p>
+ */
+public class AccessibilityNodeProviderActivity extends Activity {
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.accessibility_node_provider);
+ }
+
+ /**
+ * This class presents a View that is composed of three virtual children
+ * each of which is drawn with a different color and represents a region
+ * of the View that has different semantics compared to other such regions.
+ * While the virtual view tree exposed by this class is one level deep
+ * for simplicity, there is no bound on the complexity of that virtual
+ * sub-tree.
+ */
+ public static class VirtualSubtreeRootView extends View {
+
+ /** Paint object for drawing the virtual sub-tree */
+ private final Paint mPaint = new Paint();
+
+ /** Temporary rectangle to minimize object creation. */
+ private final Rect mTempRect = new Rect();
+
+ /** Handle to the system accessibility service. */
+ private final AccessibilityManager mAccessibilityManager;
+
+ /** The virtual children of this View. */
+ private final List<VirtualView> mChildren = new ArrayList<VirtualView>();
+
+ /** The instance of the node provider for the virtual tree - lazily instantiated. */
+ private AccessibilityNodeProvider mAccessibilityNodeProvider;
+
+ /** The last hovered child used for event dispatching. */
+ private VirtualView mLastHoveredChild;
+
+ public VirtualSubtreeRootView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mAccessibilityManager = (AccessibilityManager) context.getSystemService(
+ Service.ACCESSIBILITY_SERVICE);
+ createVirtualChildren();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+ // Instantiate the provide only when requested. Since the system
+ // will call this method multiple times it is a good practice to
+ // cache the provider instance.
+ if (mAccessibilityNodeProvider == null) {
+ mAccessibilityNodeProvider = new VirtualDescendantsProvider();
+ }
+ return mAccessibilityNodeProvider;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean dispatchHoverEvent(MotionEvent event) {
+ // This implementation assumes that the virtual children
+ // cannot overlap and are always visible. Do NOT use this
+ // code as a reference of how to implement hover event
+ // dispatch. Instead, refer to ViewGroup#dispatchHoverEvent.
+ boolean handled = false;
+ List<VirtualView> children = mChildren;
+ final int childCount = children.size();
+ for (int i = 0; i < childCount; i++) {
+ VirtualView child = children.get(i);
+ Rect childBounds = child.mBounds;
+ final int childCoordsX = (int) event.getX() + getScrollX();
+ final int childCoordsY = (int) event.getY() + getScrollY();
+ if (!childBounds.contains(childCoordsX, childCoordsY)) {
+ continue;
+ }
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER: {
+ mLastHoveredChild = child;
+ handled |= onHoverVirtualView(child, event);
+ event.setAction(action);
+ } break;
+ case MotionEvent.ACTION_HOVER_MOVE: {
+ if (child == mLastHoveredChild) {
+ handled |= onHoverVirtualView(child, event);
+ event.setAction(action);
+ } else {
+ MotionEvent eventNoHistory = event.getHistorySize() > 0
+ ? MotionEvent.obtainNoHistory(event) : event;
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ onHoverVirtualView(mLastHoveredChild, eventNoHistory);
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
+ onHoverVirtualView(child, eventNoHistory);
+ mLastHoveredChild = child;
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_MOVE);
+ handled |= onHoverVirtualView(child, eventNoHistory);
+ if (eventNoHistory != event) {
+ eventNoHistory.recycle();
+ } else {
+ event.setAction(action);
+ }
+ }
+ } break;
+ case MotionEvent.ACTION_HOVER_EXIT: {
+ mLastHoveredChild = null;
+ handled |= onHoverVirtualView(child, event);
+ event.setAction(action);
+ } break;
+ }
+ }
+ if (!handled) {
+ handled |= onHoverEvent(event);
+ }
+ return handled;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ // The virtual children are ordered horizontally next to
+ // each other and take the entire space of this View.
+ int offsetX = 0;
+ List<VirtualView> children = mChildren;
+ final int childCount = children.size();
+ for (int i = 0; i < childCount; i++) {
+ VirtualView child = children.get(i);
+ Rect childBounds = child.mBounds;
+ childBounds.set(offsetX, 0, offsetX + childBounds.width(), childBounds.height());
+ offsetX += childBounds.width();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // The virtual children are ordered horizontally next to
+ // each other and take the entire space of this View.
+ int width = 0;
+ int height = 0;
+ List<VirtualView> children = mChildren;
+ final int childCount = children.size();
+ for (int i = 0; i < childCount; i++) {
+ VirtualView child = children.get(i);
+ width += child.mBounds.width();
+ height = Math.max(height, child.mBounds.height());
+ }
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // Draw the virtual children with the reusable Paint object
+ // and with the bounds and color which are child specific.
+ Rect drawingRect = mTempRect;
+ List<VirtualView> children = mChildren;
+ final int childCount = children.size();
+ for (int i = 0; i < childCount; i++) {
+ VirtualView child = children.get(i);
+ drawingRect.set(child.mBounds);
+ mPaint.setColor(child.mColor);
+ mPaint.setAlpha(child.mAlpha);
+ canvas.drawRect(drawingRect, mPaint);
+ }
+ }
+
+ /**
+ * Creates the virtual children of this View.
+ */
+ private void createVirtualChildren() {
+ // The virtual portion of the tree is one level deep. Note
+ // that implementations can use any way of representing and
+ // drawing virtual view.
+ VirtualView firstChild = new VirtualView(0, new Rect(0, 0, 150, 150), Color.RED,
+ "Virtual view 1");
+ mChildren.add(firstChild);
+ VirtualView secondChild = new VirtualView(1, new Rect(0, 0, 150, 150), Color.GREEN,
+ "Virtual view 2");
+ mChildren.add(secondChild);
+ VirtualView thirdChild = new VirtualView(2, new Rect(0, 0, 150, 150), Color.BLUE,
+ "Virtual view 3");
+ mChildren.add(thirdChild);
+ }
+
+ /**
+ * Set the selected state of a virtual view.
+ *
+ * @param virtualView The virtual view whose selected state to set.
+ * @param selected Whether the virtual view is selected.
+ */
+ private void setVirtualViewSelected(VirtualView virtualView, boolean selected) {
+ virtualView.mAlpha = selected ? VirtualView.ALPHA_SELECTED : VirtualView.ALPHA_NOT_SELECTED;
+ }
+
+ /**
+ * Handle a hover over a virtual view.
+ *
+ * @param virtualView The virtual view over which is hovered.
+ * @param event The event to dispatch.
+ * @return Whether the event was handled.
+ */
+ private boolean onHoverVirtualView(VirtualView virtualView, MotionEvent event) {
+ // The implementation of hover event dispatch can be implemented
+ // in any way that is found suitable. However, each virtual View
+ // should fire a corresponding accessibility event whose source
+ // is that virtual view. Accessibility services get the event source
+ // as the entry point of the APIs for querying the window content.
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER: {
+ sendAccessibilityEventForVirtualView(virtualView,
+ AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ } break;
+ case MotionEvent.ACTION_HOVER_EXIT: {
+ sendAccessibilityEventForVirtualView(virtualView,
+ AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ } break;
+ }
+ return true;
+ }
+
+ /**
+ * Sends a properly initialized accessibility event for a virtual view..
+ *
+ * @param virtualView The virtual view.
+ * @param eventType The type of the event to send.
+ */
+ private void sendAccessibilityEventForVirtualView(VirtualView virtualView, int eventType) {
+ // If touch exploration, i.e. the user gets feedback while touching
+ // the screen, is enabled we fire accessibility events.
+ if (mAccessibilityManager.isTouchExplorationEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.setPackageName(getContext().getPackageName());
+ event.setClassName(virtualView.getClass().getName());
+ event.setSource(VirtualSubtreeRootView.this, virtualView.mId);
+ event.getText().add(virtualView.mText);
+ getParent().requestSendAccessibilityEvent(VirtualSubtreeRootView.this, event);
+ }
+ }
+
+ /**
+ * Finds a virtual view given its id.
+ *
+ * @param id The virtual view id.
+ * @return The found virtual view.
+ */
+ private VirtualView findVirtualViewById(int id) {
+ List<VirtualView> children = mChildren;
+ final int childCount = children.size();
+ for (int i = 0; i < childCount; i++) {
+ VirtualView child = children.get(i);
+ if (child.mId == id) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Represents a virtual View.
+ */
+ private class VirtualView {
+ public static final int ALPHA_SELECTED = 255;
+ public static final int ALPHA_NOT_SELECTED = 127;
+
+ public final int mId;
+ public final int mColor;
+ public final Rect mBounds;
+ public final String mText;
+ public int mAlpha;
+
+ public VirtualView(int id, Rect bounds, int color, String text) {
+ mId = id;
+ mColor = color;
+ mBounds = bounds;
+ mText = text;
+ mAlpha = ALPHA_NOT_SELECTED;
+ }
+ }
+
+ /**
+ * This is the provider that exposes the virtual View tree to accessibility
+ * services. From the perspective of an accessibility service the
+ * {@link AccessibilityNodeInfo}s it receives while exploring the sub-tree
+ * rooted at this View will be the same as the ones it received while
+ * exploring a View containing a sub-tree composed of real Views.
+ */
+ private class VirtualDescendantsProvider extends AccessibilityNodeProvider {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+ AccessibilityNodeInfo info = null;
+ if (virtualViewId == View.NO_ID) {
+ // We are requested to create an AccessibilityNodeInfo describing
+ // this View, i.e. the root of the virtual sub-tree. Note that the
+ // host View has an AccessibilityNodeProvider which means that this
+ // provider is responsible for creating the node info for that root.
+ info = AccessibilityNodeInfo.obtain(VirtualSubtreeRootView.this);
+ onInitializeAccessibilityNodeInfo(info);
+ // Add the virtual children of the root View.
+ List<VirtualView> children = mChildren;
+ final int childCount = children.size();
+ for (int i = 0; i < childCount; i++) {
+ VirtualView child = children.get(i);
+ info.addChild(VirtualSubtreeRootView.this, child.mId);
+ }
+ } else {
+ // Find the view that corresponds to the given id.
+ VirtualView virtualView = findVirtualViewById(virtualViewId);
+ if (virtualView == null) {
+ return null;
+ }
+ // Obtain and initialize an AccessibilityNodeInfo with
+ // information about the virtual view.
+ info = AccessibilityNodeInfo.obtain();
+ info.addAction(AccessibilityNodeInfo.ACTION_SELECT);
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);
+ info.setPackageName(getContext().getPackageName());
+ info.setClassName(virtualView.getClass().getName());
+ info.setSource(VirtualSubtreeRootView.this, virtualViewId);
+ info.setBoundsInParent(virtualView.mBounds);
+ info.setParent(VirtualSubtreeRootView.this);
+ info.setText(virtualView.mText);
+ }
+ return info;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched,
+ int virtualViewId) {
+ if (TextUtils.isEmpty(searched)) {
+ return Collections.emptyList();
+ }
+ String searchedLowerCase = searched.toLowerCase();
+ List<AccessibilityNodeInfo> result = null;
+ if (virtualViewId == View.NO_ID) {
+ // If the search is from the root, i.e. this View, go over the virtual
+ // children and look for ones that contain the searched string since
+ // this View does not contain text itself.
+ List<VirtualView> children = mChildren;
+ final int childCount = children.size();
+ for (int i = 0; i < childCount; i++) {
+ VirtualView child = children.get(i);
+ String textToLowerCase = child.mText.toLowerCase();
+ if (textToLowerCase.contains(searchedLowerCase)) {
+ if (result == null) {
+ result = new ArrayList<AccessibilityNodeInfo>();
+ }
+ result.add(createAccessibilityNodeInfo(child.mId));
+ }
+ }
+ } else {
+ // If the search is from a virtual view, find the view. Since the tree
+ // is one level deep we add a node info for the child to the result if
+ // the child contains the searched text.
+ VirtualView virtualView = findVirtualViewById(virtualViewId);
+ if (virtualView != null) {
+ String textToLowerCase = virtualView.mText.toLowerCase();
+ if (textToLowerCase.contains(searchedLowerCase)) {
+ result = new ArrayList<AccessibilityNodeInfo>();
+ result.add(createAccessibilityNodeInfo(virtualViewId));
+ }
+ }
+ }
+ if (result == null) {
+ return Collections.emptyList();
+ }
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean performAccessibilityAction(int action, int virtualViewId) {
+ if (virtualViewId == View.NO_ID) {
+ // Perform the action on the host View.
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SELECT:
+ if (!isSelected()) {
+ setSelected(true);
+ return isSelected();
+ }
+ break;
+ case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION:
+ if (isSelected()) {
+ setSelected(false);
+ return !isSelected();
+ }
+ break;
+ }
+ } else {
+ // Find the view that corresponds to the given id.
+ VirtualView child = findVirtualViewById(virtualViewId);
+ if (child == null) {
+ return false;
+ }
+ // Perform the action on a virtual view.
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SELECT:
+ setVirtualViewSelected(child, true);
+ invalidate();
+ return true;
+ case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION:
+ setVirtualViewSelected(child, false);
+ invalidate();
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+ }
+}
diff --git a/samples/ApiDemos/src/com/example/android/apis/accessibility/_index.html b/samples/ApiDemos/src/com/example/android/apis/accessibility/_index.html
new file mode 100644
index 0000000..87aea08
--- /dev/null
+++ b/samples/ApiDemos/src/com/example/android/apis/accessibility/_index.html
@@ -0,0 +1,9 @@
+<h3 id="ActionBar">Accessibility</h3>
+<dl>
+ <dt><a href="AccessibilityNodeProviderActivity.html">Accessibility Node Provider</a></dt>
+ <dd>Demonstrates how to develop an accessibility node provider which manages a virtual
+ View tree reported to accessibility services. The virtual subtree is rooted at a View
+ that draws complex content and reports itself as a tree of virtual views, thus conveying
+ its logical structure.
+ </dd>
+</dl>