16e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)/*
26e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) * Copyright (C) 2013 DroidDriver committers
36e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) *
46e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) * Licensed under the Apache License, Version 2.0 (the "License");
56e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) * you may not use this file except in compliance with the License.
66e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) * You may obtain a copy of the License at
76e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) *
86e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) *      http://www.apache.org/licenses/LICENSE-2.0
96e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) *
106e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) * Unless required by applicable law or agreed to in writing, software
116e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) * distributed under the License is distributed on an "AS IS" BASIS,
126e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
136e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) * See the License for the specific language governing permissions and
146e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) * limitations under the License.
156e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) */
166e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
176e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)package com.google.android.droiddriver.uiautomation;
186e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
196e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import static com.google.android.droiddriver.util.Strings.charSequenceToString;
206e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
216e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import android.app.UiAutomation;
226e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import android.app.UiAutomation.AccessibilityEventFilter;
236e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import android.graphics.Rect;
246e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import android.view.accessibility.AccessibilityEvent;
256e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import android.view.accessibility.AccessibilityNodeInfo;
266e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
276e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import com.google.android.droiddriver.actions.InputInjector;
286e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import com.google.android.droiddriver.base.BaseUiElement;
296e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import com.google.android.droiddriver.finders.Attribute;
306e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
316e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import com.google.android.droiddriver.util.Preconditions;
321320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
331320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucciimport java.util.ArrayList;
346e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import java.util.Collections;
356e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import java.util.EnumMap;
366e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import java.util.List;
376e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import java.util.Map;
386e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import java.util.concurrent.FutureTask;
396e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)import java.util.concurrent.TimeoutException;
406e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
416e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)/**
426e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) * A UiElement that gets attributes via the Accessibility API.
436e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles) */
446e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)public class UiAutomationElement extends BaseUiElement<AccessibilityNodeInfo, UiAutomationElement> {
451320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  private static final AccessibilityEventFilter ANY_EVENT_FILTER = new AccessibilityEventFilter() {
461320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    @Override
476e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)    public boolean accept(AccessibilityEvent arg0) {
486e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)      return true;
496e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)    }
506e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)  };
516e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
526e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)  private final AccessibilityNodeInfo node;
536e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)  private final UiAutomationContext context;
546e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)  private final Map<Attribute, Object> attributes;
556e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)  private final boolean visible;
566e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)  private final Rect visibleBounds;
576e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)  private final UiAutomationElement parent;
586e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)  private final List<UiAutomationElement> children;
596e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
606e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)  /**
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