UiAutomationElement.java revision d2abd0b28789a4a187343b0485e2b8e3fc9ef7ac
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.TextUtils.charSequenceToString; 20 21import android.app.UiAutomation.AccessibilityEventFilter; 22import android.graphics.Rect; 23import android.view.accessibility.AccessibilityEvent; 24import android.view.accessibility.AccessibilityNodeInfo; 25 26import com.google.android.droiddriver.actions.InputInjector; 27import com.google.android.droiddriver.base.BaseUiElement; 28import com.google.android.droiddriver.finders.Attribute; 29import com.google.common.base.Preconditions; 30import com.google.common.collect.ImmutableList; 31import com.google.common.collect.ImmutableMap; 32import com.google.common.collect.Lists; 33import com.google.common.collect.Maps; 34 35import java.util.List; 36import java.util.Map; 37import java.util.concurrent.FutureTask; 38import java.util.concurrent.TimeoutException; 39 40/** 41 * A UiElement that is backed by an {@link AccessibilityNodeInfo}. 42 */ 43public class UiAutomationElement extends BaseUiElement { 44 private static final AccessibilityEventFilter ANY_EVENT_FILTER = new AccessibilityEventFilter() { 45 @Override 46 public boolean accept(AccessibilityEvent arg0) { 47 return true; 48 } 49 }; 50 51 private final UiAutomationContext context; 52 private final Map<Attribute, Object> attributes; 53 private final boolean visible; 54 private final Rect visibleBounds; 55 private final UiAutomationElement parent; 56 private final List<UiAutomationElement> children; 57 58 /** 59 * A snapshot of all attributes is taken at construction. The attributes of a 60 * {@code UiAutomationElement} instance are immutable. If the underlying 61 * {@link AccessibilityNodeInfo} is updated, a new {@code UiAutomationElement} 62 * instance will be created in 63 * {@link com.google.android.droiddriver.DroidDriver#refreshUiElementTree}. 64 */ 65 public UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node, 66 UiAutomationElement parent) { 67 this.context = Preconditions.checkNotNull(context); 68 Preconditions.checkNotNull(node); 69 this.parent = parent; 70 71 Map<Attribute, Object> attribs = Maps.newEnumMap(Attribute.class); 72 put(attribs, Attribute.PACKAGE, charSequenceToString(node.getPackageName())); 73 put(attribs, Attribute.CLASS, charSequenceToString(node.getClassName())); 74 put(attribs, Attribute.TEXT, charSequenceToString(node.getText())); 75 put(attribs, Attribute.CONTENT_DESC, charSequenceToString(node.getContentDescription())); 76 put(attribs, Attribute.RESOURCE_ID, charSequenceToString(node.getViewIdResourceName())); 77 put(attribs, Attribute.CHECKABLE, node.isCheckable()); 78 put(attribs, Attribute.CHECKED, node.isChecked()); 79 put(attribs, Attribute.CLICKABLE, node.isClickable()); 80 put(attribs, Attribute.ENABLED, node.isEnabled()); 81 put(attribs, Attribute.FOCUSABLE, node.isFocusable()); 82 put(attribs, Attribute.FOCUSED, node.isFocused()); 83 put(attribs, Attribute.LONG_CLICKABLE, node.isLongClickable()); 84 put(attribs, Attribute.PASSWORD, node.isPassword()); 85 put(attribs, Attribute.SCROLLABLE, node.isScrollable()); 86 put(attribs, Attribute.SELECTED, node.isSelected()); 87 put(attribs, Attribute.BOUNDS, getBounds(node)); 88 attributes = ImmutableMap.copyOf(attribs); 89 90 // Order matters as getVisibleBounds depends on visible 91 visible = node.isVisibleToUser(); 92 visibleBounds = getVisibleBounds(node); 93 List<UiAutomationElement> mutableChildren = buildChildren(context, node); 94 this.children = mutableChildren == null ? null : ImmutableList.copyOf(mutableChildren); 95 } 96 97 private void put(Map<Attribute, Object> attribs, Attribute key, Object value) { 98 if (value != null) { 99 attribs.put(key, value); 100 } 101 } 102 103 private List<UiAutomationElement> buildChildren(UiAutomationContext context, 104 AccessibilityNodeInfo node) { 105 List<UiAutomationElement> children; 106 int childCount = node.getChildCount(); 107 if (childCount == 0) { 108 children = null; 109 } else { 110 children = Lists.newArrayListWithExpectedSize(childCount); 111 for (int i = 0; i < childCount; i++) { 112 AccessibilityNodeInfo child = node.getChild(i); 113 if (child != null) { 114 children.add(context.getUiElement(child, this)); 115 } 116 } 117 } 118 return children; 119 } 120 121 private Rect getBounds(AccessibilityNodeInfo node) { 122 Rect rect = new Rect(); 123 node.getBoundsInScreen(rect); 124 return rect; 125 } 126 127 private Rect getVisibleBounds(AccessibilityNodeInfo node) { 128 if (!visible) { 129 return new Rect(); 130 } 131 Rect visibleBounds = getBounds(); 132 UiAutomationElement parent = getParent(); 133 Rect parentBounds; 134 while (parent != null) { 135 parentBounds = parent.getBounds(); 136 visibleBounds.intersect(parentBounds); 137 parent = parent.getParent(); 138 } 139 return visibleBounds; 140 } 141 142 @Override 143 public Rect getVisibleBounds() { 144 return visibleBounds; 145 } 146 147 @Override 148 public boolean isVisible() { 149 return visible; 150 } 151 152 @Override 153 public UiAutomationElement getParent() { 154 return parent; 155 } 156 157 @Override 158 protected List<UiAutomationElement> getChildren() { 159 return children; 160 } 161 162 @Override 163 protected Map<Attribute, Object> getAttributes() { 164 return attributes; 165 } 166 167 @Override 168 public InputInjector getInjector() { 169 return context.getInjector(); 170 } 171 172 @Override 173 protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) { 174 try { 175 context.getUiAutomation().executeAndWaitForEvent(futureTask, ANY_EVENT_FILTER, timeoutMillis); 176 } catch (TimeoutException e) { 177 // This is for sync'ing with Accessibility API on best-effort because 178 // it is not reliable. 179 // Exception is ignored here. Tests will fail anyways if this is 180 // critical. 181 // Actions should usually trigger some AccessibilityEvent's, but some 182 // widgets fail to do so, resulting in stale AccessibilityNodeInfo's. As a 183 // work-around, force to clear the AccessibilityNodeInfoCache. 184 // A legitimate case of no AccessibilityEvent is when scrolling has 185 // reached the end, but we cannot tell whether it's legitimate or the 186 // widget has bugs, so clearAccessibilityNodeInfoCache anyways. 187 context.getDriver().clearAccessibilityNodeInfoCacheHack(); 188 } 189 } 190} 191