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 io.appium.droiddriver.instrumentation; 18 19import android.content.res.Resources; 20import android.graphics.Rect; 21import android.view.View; 22import android.view.ViewGroup; 23import android.view.accessibility.AccessibilityNodeInfo; 24import android.widget.Checkable; 25import android.widget.TextView; 26 27import java.util.ArrayList; 28import java.util.Collections; 29import java.util.EnumMap; 30import java.util.HashMap; 31import java.util.List; 32import java.util.Map; 33import java.util.concurrent.Callable; 34import java.util.concurrent.FutureTask; 35 36import io.appium.droiddriver.actions.InputInjector; 37import io.appium.droiddriver.base.BaseUiElement; 38import io.appium.droiddriver.base.DroidDriverContext; 39import io.appium.droiddriver.finders.Attribute; 40import io.appium.droiddriver.util.InstrumentationUtils; 41import io.appium.droiddriver.util.Preconditions; 42 43import static io.appium.droiddriver.util.Strings.charSequenceToString; 44 45/** 46 * A UiElement that is backed by a View. 47 */ 48public class ViewElement extends BaseUiElement<View, ViewElement> { 49 private static class AttributesSnapshot implements Callable<Void> { 50 private final View view; 51 final Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(Attribute.class); 52 boolean visible; 53 Rect visibleBounds; 54 List<View> childViews; 55 56 private AttributesSnapshot(View view) { 57 this.view = view; 58 } 59 60 @Override 61 public Void call() { 62 put(Attribute.PACKAGE, view.getContext().getPackageName()); 63 put(Attribute.CLASS, getClassName()); 64 put(Attribute.TEXT, getText()); 65 put(Attribute.CONTENT_DESC, charSequenceToString(view.getContentDescription())); 66 put(Attribute.RESOURCE_ID, getResourceId()); 67 put(Attribute.CHECKABLE, view instanceof Checkable); 68 put(Attribute.CHECKED, isChecked()); 69 put(Attribute.CLICKABLE, view.isClickable()); 70 put(Attribute.ENABLED, view.isEnabled()); 71 put(Attribute.FOCUSABLE, view.isFocusable()); 72 put(Attribute.FOCUSED, view.isFocused()); 73 put(Attribute.LONG_CLICKABLE, view.isLongClickable()); 74 put(Attribute.PASSWORD, isPassword()); 75 put(Attribute.SCROLLABLE, isScrollable()); 76 if (view instanceof TextView) { 77 TextView textView = (TextView) view; 78 if (textView.hasSelection()) { 79 attribs.put(Attribute.SELECTION_START, textView.getSelectionStart()); 80 attribs.put(Attribute.SELECTION_END, textView.getSelectionEnd()); 81 } 82 } 83 put(Attribute.SELECTED, view.isSelected()); 84 put(Attribute.BOUNDS, getBounds()); 85 86 // Order matters as setVisible() depends on setVisibleBounds(). 87 this.visibleBounds = getVisibleBounds(); 88 // isShown() checks the visibility flag of this view and ancestors; it 89 // needs to have the VISIBLE flag as well as non-empty bounds to be 90 // visible. 91 this.visible = view.isShown() && !visibleBounds.isEmpty(); 92 setChildViews(); 93 return null; 94 } 95 96 private void put(Attribute key, Object value) { 97 if (value != null) { 98 attribs.put(key, value); 99 } 100 } 101 102 private String getText() { 103 if (!(view instanceof TextView)) { 104 return null; 105 } 106 return charSequenceToString(((TextView) view).getText()); 107 } 108 109 private String getClassName() { 110 String className = view.getClass().getName(); 111 return CLASS_NAME_OVERRIDES.containsKey(className) ? CLASS_NAME_OVERRIDES.get(className) 112 : className; 113 } 114 115 private String getResourceId() { 116 if (view.getId() != View.NO_ID && view.getResources() != null) { 117 try { 118 return charSequenceToString(view.getResources().getResourceName(view.getId())); 119 } catch (Resources.NotFoundException nfe) { 120 /* ignore */ 121 } 122 } 123 return null; 124 } 125 126 private boolean isChecked() { 127 return view instanceof Checkable && ((Checkable) view).isChecked(); 128 } 129 130 private boolean isScrollable() { 131 // TODO: find a meaningful implementation 132 return true; 133 } 134 135 private boolean isPassword() { 136 // TODO: find a meaningful implementation 137 return false; 138 } 139 140 private Rect getBounds() { 141 Rect rect = new Rect(); 142 int[] xy = new int[2]; 143 view.getLocationOnScreen(xy); 144 rect.set(xy[0], xy[1], xy[0] + view.getWidth(), xy[1] + view.getHeight()); 145 return rect; 146 } 147 148 private Rect getVisibleBounds() { 149 Rect visibleBounds = new Rect(); 150 if (!view.isShown() || !view.getGlobalVisibleRect(visibleBounds)) { 151 visibleBounds.setEmpty(); 152 } 153 int[] xyScreen = new int[2]; 154 view.getLocationOnScreen(xyScreen); 155 int[] xyWindow = new int[2]; 156 view.getLocationInWindow(xyWindow); 157 int windowLeft = xyScreen[0] - xyWindow[0]; 158 int windowTop = xyScreen[1] - xyWindow[1]; 159 160 // Bounds are relative to root view; adjust to screen coordinates. 161 visibleBounds.offset(windowLeft, windowTop); 162 return visibleBounds; 163 } 164 165 private void setChildViews() { 166 if (!(view instanceof ViewGroup)) { 167 return; 168 } 169 ViewGroup group = (ViewGroup) view; 170 int childCount = group.getChildCount(); 171 childViews = new ArrayList<View>(childCount); 172 for (int i = 0; i < childCount; i++) { 173 View child = group.getChildAt(i); 174 if (child != null) { 175 childViews.add(child); 176 } 177 } 178 } 179 } 180 181 private static final Map<String, String> CLASS_NAME_OVERRIDES = new HashMap<String, String>(); 182 183 /** 184 * Typically users find the class name to use in tests using SDK tool 185 * uiautomatorviewer. This name is returned by 186 * {@link AccessibilityNodeInfo#getClassName}. If the app uses custom View 187 * classes that do not call {@link AccessibilityNodeInfo#setClassName} with 188 * the actual class name, different types of drivers see different class names 189 * (InstrumentationDriver sees the actual class name, while UiAutomationDriver 190 * sees {@link AccessibilityNodeInfo#getClassName}). 191 * <p> 192 * If tests fail with InstrumentationDriver, find the actual class name by 193 * examining app code or by calling 194 * {@link io.appium.droiddriver.DroidDriver#dumpUiElementTree}, then 195 * call this method in setUp to override it with the class name seen in 196 * uiautomatorviewer. 197 * </p> 198 * A better solution is to use resource-id instead of classname, which is an 199 * implementation detail and subject to change. 200 */ 201 public static void overrideClassName(String actualClassName, String overridingClassName) { 202 CLASS_NAME_OVERRIDES.put(actualClassName, overridingClassName); 203 } 204 205 private final DroidDriverContext<View, ViewElement> context; 206 private final View view; 207 private final Map<Attribute, Object> attributes; 208 private final boolean visible; 209 private final Rect visibleBounds; 210 private final ViewElement parent; 211 private final List<ViewElement> children; 212 213 /** 214 * A snapshot of all attributes is taken at construction. The attributes of a 215 * {@code ViewElement} instance are immutable. If the underlying view is 216 * updated, a new {@code ViewElement} instance will be created in 217 * {@link io.appium.droiddriver.DroidDriver#refreshUiElementTree}. 218 */ 219 public ViewElement(DroidDriverContext<View, ViewElement> context, View view, ViewElement parent) { 220 this.context = Preconditions.checkNotNull(context); 221 this.view = Preconditions.checkNotNull(view); 222 this.parent = parent; 223 AttributesSnapshot attributesSnapshot = new AttributesSnapshot(view); 224 InstrumentationUtils.runOnMainSyncWithTimeout(attributesSnapshot); 225 226 attributes = Collections.unmodifiableMap(attributesSnapshot.attribs); 227 this.visibleBounds = attributesSnapshot.visibleBounds; 228 this.visible = attributesSnapshot.visible; 229 if (attributesSnapshot.childViews == null) { 230 this.children = null; 231 } else { 232 List<ViewElement> children = new ArrayList<ViewElement>(attributesSnapshot.childViews.size()); 233 for (View childView : attributesSnapshot.childViews) { 234 children.add(context.getElement(childView, this)); 235 } 236 this.children = Collections.unmodifiableList(children); 237 } 238 } 239 240 @Override 241 public Rect getVisibleBounds() { 242 return visibleBounds; 243 } 244 245 @Override 246 public boolean isVisible() { 247 return visible; 248 } 249 250 @Override 251 public ViewElement getParent() { 252 return parent; 253 } 254 255 @Override 256 protected List<ViewElement> getChildren() { 257 return children; 258 } 259 260 @Override 261 protected Map<Attribute, Object> getAttributes() { 262 return attributes; 263 } 264 265 @Override 266 public InputInjector getInjector() { 267 return context.getDriver().getInjector(); 268 } 269 270 @Override 271 protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) { 272 futureTask.run(); 273 InstrumentationUtils.tryWaitForIdleSync(timeoutMillis); 274 } 275 276 @Override 277 public View getRawElement() { 278 return view; 279 } 280} 281