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