/* * 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 performAction(int virtualViewId, int action, Bundle arguments) { 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; } } } }