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 com.google.android.droiddriver.instrumentation; 18 19 import static com.google.android.droiddriver.util.TextUtils.charSequenceToString; 20 21 import android.content.res.Resources; 22 import android.graphics.Rect; 23 import android.util.Log; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.view.ViewParent; 27 import android.view.accessibility.AccessibilityNodeInfo; 28 import android.widget.Checkable; 29 import android.widget.TextView; 30 31 import com.google.android.droiddriver.InputInjector; 32 import com.google.android.droiddriver.base.AbstractUiElement; 33 import com.google.android.droiddriver.util.Logs; 34 import com.google.common.base.Preconditions; 35 import com.google.common.collect.Maps; 36 37 import java.util.Map; 38 39 /** 40 * A UiElement that is backed by a View. 41 */ 42 // TODO: always accessing view on the UI thread even when only get access is 43 // needed -- the field may be in the middle of updating. 44 public class ViewElement extends AbstractUiElement { 45 private static final Map<String, String> CLASS_NAME_OVERRIDES = Maps.newHashMap(); 46 47 private final InstrumentationContext context; 48 private final View view; 49 50 /** 51 * Typically users find the class name to use in tests using SDK tool 52 * uiautomatorviewer. This name is returned by 53 * {@link AccessibilityNodeInfo#getClassName}. If the app uses custom View 54 * classes that do not call {@link AccessibilityNodeInfo#setClassName} with 55 * the actual class name, different types of drivers see different class names 56 * (InstrumentationDriver sees the actual class name, while UiAutomationDriver 57 * sees {@link AccessibilityNodeInfo#getClassName}). 58 * <p> 59 * If tests fail with InstrumentationDriver, find the actual class name by 60 * examining app code or by calling 61 * {@link com.google.android.droiddriver.DroidDriver#dumpUiElementTree}, then 62 * call this method in setUp to override it with the class name seen in 63 * uiautomatorviewer. 64 */ 65 public static void overrideClassName(String actualClassName, String overridingClassName) { 66 CLASS_NAME_OVERRIDES.put(actualClassName, overridingClassName); 67 } 68 69 public ViewElement(InstrumentationContext context, View view) { 70 this.context = Preconditions.checkNotNull(context); 71 this.view = Preconditions.checkNotNull(view); 72 } 73 74 @Override 75 public String getText() { 76 if (!(view instanceof TextView)) { 77 return null; 78 } 79 return charSequenceToString(((TextView) view).getText()); 80 } 81 82 @Override 83 public String getContentDescription() { 84 return charSequenceToString(view.getContentDescription()); 85 } 86 87 @Override 88 public String getClassName() { 89 String className = view.getClass().getName(); 90 return CLASS_NAME_OVERRIDES.containsKey(className) ? CLASS_NAME_OVERRIDES.get(className) 91 : className; 92 } 93 94 @Override 95 public String getResourceId() { 96 if (view.getId() != View.NO_ID && view.getResources() != null) { 97 try { 98 return charSequenceToString(view.getResources().getResourceName(view.getId())); 99 } catch (Resources.NotFoundException nfe) { 100 /* ignore */ 101 } 102 } 103 return null; 104 } 105 106 @Override 107 public String getPackageName() { 108 return view.getContext().getPackageName(); 109 } 110 111 @Override 112 public InputInjector getInjector() { 113 return context.getInjector(); 114 } 115 116 @Override 117 public boolean isVisible() { 118 // isShown() checks the visibility flag of this view and ancestors; it needs 119 // to have the VISIBLE flag as well as non-empty bounds to be visible. 120 return view.isShown() && !getVisibleBounds().isEmpty(); 121 } 122 123 @Override 124 public boolean isCheckable() { 125 return view instanceof Checkable; 126 } 127 128 @Override 129 public boolean isChecked() { 130 if (!isCheckable()) { 131 return false; 132 } 133 return ((Checkable) view).isChecked(); 134 } 135 136 @Override 137 public boolean isClickable() { 138 return view.isClickable(); 139 } 140 141 @Override 142 public boolean isEnabled() { 143 return view.isEnabled(); 144 } 145 146 @Override 147 public boolean isFocusable() { 148 return view.isFocusable(); 149 } 150 151 @Override 152 public boolean isFocused() { 153 return view.isFocused(); 154 } 155 156 @Override 157 public boolean isScrollable() { 158 // TODO: find a meaningful implementation 159 return true; 160 } 161 162 @Override 163 public boolean isLongClickable() { 164 return view.isLongClickable(); 165 } 166 167 @Override 168 public boolean isPassword() { 169 // TODO: find a meaningful implementation 170 return false; 171 } 172 173 @Override 174 public boolean isSelected() { 175 return view.isSelected(); 176 } 177 178 @Override 179 public Rect getBounds() { 180 Rect rect = new Rect(); 181 int[] xy = new int[2]; 182 view.getLocationOnScreen(xy); 183 rect.set(xy[0], xy[1], xy[0] + view.getWidth(), xy[1] + view.getHeight()); 184 return rect; 185 } 186 187 @Override 188 public Rect getVisibleBounds() { 189 Rect visibleBounds = new Rect(); 190 if (!view.getGlobalVisibleRect(visibleBounds)) { 191 Logs.log(Log.VERBOSE, "View is invisible: " + toString()); 192 visibleBounds.setEmpty(); 193 } 194 int[] xy = new int[2]; 195 view.getLocationOnScreen(xy); 196 // Bounds are relative to root view; adjust to screen coordinates. 197 visibleBounds.offsetTo(xy[0], xy[1]); 198 return visibleBounds; 199 } 200 201 @Override 202 public int getChildCount() { 203 if (!(view instanceof ViewGroup)) { 204 return 0; 205 } 206 return ((ViewGroup) view).getChildCount(); 207 } 208 209 @Override 210 public ViewElement getChild(int index) { 211 if (!(view instanceof ViewGroup)) { 212 return null; 213 } 214 View child = ((ViewGroup) view).getChildAt(index); 215 return child == null ? null : context.getUiElement(child); 216 } 217 218 @Override 219 public ViewElement getParent() { 220 ViewParent parent = view.getParent(); 221 if (!(parent instanceof View)) { 222 return null; 223 } 224 return context.getUiElement((View) parent); 225 } 226 } 227