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