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.Strings.charSequenceToString;
20
21import android.app.UiAutomation;
22import android.app.UiAutomation.AccessibilityEventFilter;
23import android.graphics.Rect;
24import android.view.accessibility.AccessibilityEvent;
25import android.view.accessibility.AccessibilityNodeInfo;
26
27import com.google.android.droiddriver.actions.InputInjector;
28import com.google.android.droiddriver.base.BaseUiElement;
29import com.google.android.droiddriver.finders.Attribute;
30import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
31import com.google.android.droiddriver.util.Preconditions;
32
33import java.util.ArrayList;
34import java.util.Collections;
35import java.util.EnumMap;
36import java.util.List;
37import java.util.Map;
38import java.util.concurrent.FutureTask;
39import java.util.concurrent.TimeoutException;
40
41/**
42 * A UiElement that gets attributes via the Accessibility API.
43 */
44public 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