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 static io.appium.droiddriver.util.Strings.charSequenceToString; 20 21 import android.annotation.TargetApi; 22 import android.app.UiAutomation; 23 import android.app.UiAutomation.AccessibilityEventFilter; 24 import android.graphics.Rect; 25 import android.view.accessibility.AccessibilityEvent; 26 import android.view.accessibility.AccessibilityNodeInfo; 27 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.EnumMap; 31 import java.util.List; 32 import java.util.Map; 33 import java.util.concurrent.FutureTask; 34 import java.util.concurrent.TimeoutException; 35 36 import io.appium.droiddriver.actions.InputInjector; 37 import io.appium.droiddriver.base.BaseUiElement; 38 import io.appium.droiddriver.finders.Attribute; 39 import io.appium.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable; 40 import io.appium.droiddriver.util.Preconditions; 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 */ 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 findVisibleBounds depends on visible 100 visible = node.isVisibleToUser(); 101 visibleBounds = findVisibleBounds(); 102 List<UiAutomationElement> mutableChildren = buildChildren(node); 103 this.children = mutableChildren == null ? null : Collections.unmodifiableList(mutableChildren); 104 } 105 106 private void put(Map<Attribute, Object> attribs, Attribute key, Object value) { 107 if (value != null) { 108 attribs.put(key, value); 109 } 110 } 111 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 129 private Rect getBounds(AccessibilityNodeInfo node) { 130 Rect rect = new Rect(); 131 node.getBoundsInScreen(rect); 132 return rect; 133 } 134 135 private Rect findVisibleBounds() { 136 if (!visible) { 137 return new Rect(); 138 } 139 Rect foundBounds = getBounds(); 140 UiAutomationElement parent = getParent(); 141 while (parent != null) { 142 if (!foundBounds.intersect(parent.getBounds())) { 143 return new Rect(); 144 } 145 parent = parent.getParent(); 146 } 147 return foundBounds; 148 } 149 150 @Override 151 public Rect getVisibleBounds() { 152 return visibleBounds; 153 } 154 155 @Override 156 public boolean isVisible() { 157 return visible; 158 } 159 160 @Override 161 public UiAutomationElement getParent() { 162 return parent; 163 } 164 165 @Override 166 protected List<UiAutomationElement> getChildren() { 167 return children; 168 } 169 170 @Override 171 protected Map<Attribute, Object> getAttributes() { 172 return attributes; 173 } 174 175 @Override 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 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 212 public AccessibilityNodeInfo getRawElement() { 213 return node; 214 } 215 } 216