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