Home | History | Annotate | Download | only in accessibility
      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
     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 
     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
    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
    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
    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
    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
    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          */
    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          */
    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          */
    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          */
    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          */
    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 
    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
    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
    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
    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