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