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