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 android.annotation.TargetApi;
20 import android.app.UiAutomation;
21 import android.app.UiAutomation.AccessibilityEventFilter;
22 import android.graphics.Rect;
23 import android.view.accessibility.AccessibilityEvent;
24 import android.view.accessibility.AccessibilityNodeInfo;
25 
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.EnumMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.concurrent.FutureTask;
32 import java.util.concurrent.TimeoutException;
33 
34 import io.appium.droiddriver.actions.InputInjector;
35 import io.appium.droiddriver.base.BaseUiElement;
36 import io.appium.droiddriver.finders.Attribute;
37 import io.appium.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
38 import io.appium.droiddriver.util.Preconditions;
39 
40 import static io.appium.droiddriver.util.Strings.charSequenceToString;
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    */
UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node, UiAutomationElement parent)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 getVisibleBounds depends on visible
100     visible = node.isVisibleToUser();
101     visibleBounds = getVisibleBounds(node);
102     List<UiAutomationElement> mutableChildren = buildChildren(node);
103     this.children = mutableChildren == null ? null : Collections.unmodifiableList(mutableChildren);
104   }
105 
put(Map<Attribute, Object> attribs, Attribute key, Object value)106   private void put(Map<Attribute, Object> attribs, Attribute key, Object value) {
107     if (value != null) {
108       attribs.put(key, value);
109     }
110   }
111 
buildChildren(AccessibilityNodeInfo node)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 
getBounds(AccessibilityNodeInfo node)129   private Rect getBounds(AccessibilityNodeInfo node) {
130     Rect rect = new Rect();
131     node.getBoundsInScreen(rect);
132     return rect;
133   }
134 
getVisibleBounds(AccessibilityNodeInfo node)135   private Rect getVisibleBounds(AccessibilityNodeInfo node) {
136     if (!visible) {
137       return new Rect();
138     }
139     Rect visibleBounds = getBounds();
140     UiAutomationElement parent = getParent();
141     Rect parentBounds;
142     while (parent != null) {
143       parentBounds = parent.getBounds();
144       visibleBounds.intersect(parentBounds);
145       parent = parent.getParent();
146     }
147     return visibleBounds;
148   }
149 
150   @Override
getVisibleBounds()151   public Rect getVisibleBounds() {
152     return visibleBounds;
153   }
154 
155   @Override
isVisible()156   public boolean isVisible() {
157     return visible;
158   }
159 
160   @Override
getParent()161   public UiAutomationElement getParent() {
162     return parent;
163   }
164 
165   @Override
getChildren()166   protected List<UiAutomationElement> getChildren() {
167     return children;
168   }
169 
170   @Override
getAttributes()171   protected Map<Attribute, Object> getAttributes() {
172     return attributes;
173   }
174 
175   @Override
getInjector()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
doPerformAndWait(final FutureTask<Boolean> futureTask, final long timeoutMillis)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
getRawElement()212   public AccessibilityNodeInfo getRawElement() {
213     return node;
214   }
215 }
216