Home | History | Annotate | Download | only in uiautomation
      1 /*
      2  * Copyright (C) 2013 DroidDriver committers
      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.google.android.droiddriver.uiautomation;
     18 
     19 import static com.google.android.droiddriver.util.Strings.charSequenceToString;
     20 
     21 import android.app.UiAutomation;
     22 import android.app.UiAutomation.AccessibilityEventFilter;
     23 import android.graphics.Rect;
     24 import android.view.accessibility.AccessibilityEvent;
     25 import android.view.accessibility.AccessibilityNodeInfo;
     26 
     27 import com.google.android.droiddriver.actions.InputInjector;
     28 import com.google.android.droiddriver.base.BaseUiElement;
     29 import com.google.android.droiddriver.finders.Attribute;
     30 import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
     31 import com.google.android.droiddriver.util.Preconditions;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Collections;
     35 import java.util.EnumMap;
     36 import java.util.List;
     37 import java.util.Map;
     38 import java.util.concurrent.FutureTask;
     39 import java.util.concurrent.TimeoutException;
     40 
     41 /**
     42  * A UiElement that gets attributes via the Accessibility API.
     43  */
     44 public class UiAutomationElement extends BaseUiElement<AccessibilityNodeInfo, UiAutomationElement> {
     45   private static final AccessibilityEventFilter ANY_EVENT_FILTER = new AccessibilityEventFilter() {
     46     @Override
     47     public boolean accept(AccessibilityEvent arg0) {
     48       return true;
     49     }
     50   };
     51 
     52   private final AccessibilityNodeInfo node;
     53   private final UiAutomationContext context;
     54   private final Map<Attribute, Object> attributes;
     55   private final boolean visible;
     56   private final Rect visibleBounds;
     57   private final UiAutomationElement parent;
     58   private final List<UiAutomationElement> children;
     59 
     60   /**
     61    * A snapshot of all attributes is taken at construction. The attributes of a
     62    * {@code UiAutomationElement} instance are immutable. If the underlying
     63    * {@link AccessibilityNodeInfo} is updated, a new {@code UiAutomationElement}
     64    * instance will be created in
     65    * {@link com.google.android.droiddriver.DroidDriver#refreshUiElementTree}.
     66    */
     67   protected UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node,
     68       UiAutomationElement parent) {
     69     this.node = Preconditions.checkNotNull(node);
     70     this.context = Preconditions.checkNotNull(context);
     71     this.parent = parent;
     72 
     73     Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(Attribute.class);
     74     put(attribs, Attribute.PACKAGE, charSequenceToString(node.getPackageName()));
     75     put(attribs, Attribute.CLASS, charSequenceToString(node.getClassName()));
     76     put(attribs, Attribute.TEXT, charSequenceToString(node.getText()));
     77     put(attribs, Attribute.CONTENT_DESC, charSequenceToString(node.getContentDescription()));
     78     put(attribs, Attribute.RESOURCE_ID, charSequenceToString(node.getViewIdResourceName()));
     79     put(attribs, Attribute.CHECKABLE, node.isCheckable());
     80     put(attribs, Attribute.CHECKED, node.isChecked());
     81     put(attribs, Attribute.CLICKABLE, node.isClickable());
     82     put(attribs, Attribute.ENABLED, node.isEnabled());
     83     put(attribs, Attribute.FOCUSABLE, node.isFocusable());
     84     put(attribs, Attribute.FOCUSED, node.isFocused());
     85     put(attribs, Attribute.LONG_CLICKABLE, node.isLongClickable());
     86     put(attribs, Attribute.PASSWORD, node.isPassword());
     87     put(attribs, Attribute.SCROLLABLE, node.isScrollable());
     88     if (node.getTextSelectionStart() >= 0
     89         && node.getTextSelectionStart() != node.getTextSelectionEnd()) {
     90       attribs.put(Attribute.SELECTION_START, node.getTextSelectionStart());
     91       attribs.put(Attribute.SELECTION_END, node.getTextSelectionEnd());
     92     }
     93     put(attribs, Attribute.SELECTED, node.isSelected());
     94     put(attribs, Attribute.BOUNDS, getBounds(node));
     95     attributes = Collections.unmodifiableMap(attribs);
     96 
     97     // Order matters as getVisibleBounds depends on visible
     98     visible = node.isVisibleToUser();
     99     visibleBounds = getVisibleBounds(node);
    100     List<UiAutomationElement> mutableChildren = buildChildren(node);
    101     this.children = mutableChildren == null ? null : Collections.unmodifiableList(mutableChildren);
    102   }
    103 
    104   private void put(Map<Attribute, Object> attribs, Attribute key, Object value) {
    105     if (value != null) {
    106       attribs.put(key, value);
    107     }
    108   }
    109 
    110   private List<UiAutomationElement> buildChildren(AccessibilityNodeInfo node) {
    111     List<UiAutomationElement> children;
    112     int childCount = node.getChildCount();
    113     if (childCount == 0) {
    114       children = null;
    115     } else {
    116       children = new ArrayList<UiAutomationElement>(childCount);
    117       for (int i = 0; i < childCount; i++) {
    118         AccessibilityNodeInfo child = node.getChild(i);
    119         if (child != null) {
    120           children.add(context.getElement(child, this));
    121         }
    122       }
    123     }
    124     return children;
    125   }
    126 
    127   private Rect getBounds(AccessibilityNodeInfo node) {
    128     Rect rect = new Rect();
    129     node.getBoundsInScreen(rect);
    130     return rect;
    131   }
    132 
    133   private Rect getVisibleBounds(AccessibilityNodeInfo node) {
    134     if (!visible) {
    135       return new Rect();
    136     }
    137     Rect visibleBounds = getBounds();
    138     UiAutomationElement parent = getParent();
    139     Rect parentBounds;
    140     while (parent != null) {
    141       parentBounds = parent.getBounds();
    142       visibleBounds.intersect(parentBounds);
    143       parent = parent.getParent();
    144     }
    145     return visibleBounds;
    146   }
    147 
    148   @Override
    149   public Rect getVisibleBounds() {
    150     return visibleBounds;
    151   }
    152 
    153   @Override
    154   public boolean isVisible() {
    155     return visible;
    156   }
    157 
    158   @Override
    159   public UiAutomationElement getParent() {
    160     return parent;
    161   }
    162 
    163   @Override
    164   protected List<UiAutomationElement> getChildren() {
    165     return children;
    166   }
    167 
    168   @Override
    169   protected Map<Attribute, Object> getAttributes() {
    170     return attributes;
    171   }
    172 
    173   @Override
    174   public InputInjector getInjector() {
    175     return context.getDriver().getInjector();
    176   }
    177 
    178   /**
    179    * Note: This implementation of {@code doPerformAndWait} clears the
    180    * {@code AccessibilityEvent} queue.
    181    */
    182   @Override
    183   protected void doPerformAndWait(final FutureTask<Boolean> futureTask, final long timeoutMillis) {
    184     context.callUiAutomation(new UiAutomationCallable<Void>() {
    185 
    186       @Override
    187       public Void call(UiAutomation uiAutomation) {
    188         try {
    189           uiAutomation.executeAndWaitForEvent(futureTask, ANY_EVENT_FILTER, timeoutMillis);
    190         } catch (TimeoutException e) {
    191           // This is for sync'ing with Accessibility API on best-effort because
    192           // it is not reliable.
    193           // Exception is ignored here. Tests will fail anyways if this is
    194           // critical.
    195           // Actions should usually trigger some AccessibilityEvent's, but some
    196           // widgets fail to do so, resulting in stale AccessibilityNodeInfo's.
    197           // As a work-around, force to clear the AccessibilityNodeInfoCache.
    198           // A legitimate case of no AccessibilityEvent is when scrolling has
    199           // reached the end, but we cannot tell whether it's legitimate or the
    200           // widget has bugs, so clearAccessibilityNodeInfoCache anyways.
    201           context.getDriver().clearAccessibilityNodeInfoCache();
    202         }
    203         return null;
    204       }
    205 
    206     });
    207   }
    208 
    209   @Override
    210   public AccessibilityNodeInfo getRawElement() {
    211     return node;
    212   }
    213 }
    214