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.base; 18 19import android.graphics.Rect; 20import android.os.Build; 21import android.text.TextUtils; 22import android.view.KeyEvent; 23 24import java.util.ArrayList; 25import java.util.Collections; 26import java.util.List; 27import java.util.Map; 28import java.util.concurrent.Callable; 29import java.util.concurrent.FutureTask; 30 31import io.appium.droiddriver.UiElement; 32import io.appium.droiddriver.actions.Action; 33import io.appium.droiddriver.actions.EventUiElementActor; 34import io.appium.droiddriver.actions.InputInjector; 35import io.appium.droiddriver.actions.SingleKeyAction; 36import io.appium.droiddriver.actions.TextAction; 37import io.appium.droiddriver.actions.UiElementActor; 38import io.appium.droiddriver.exceptions.DroidDriverException; 39import io.appium.droiddriver.finders.Attribute; 40import io.appium.droiddriver.finders.Predicate; 41import io.appium.droiddriver.finders.Predicates; 42import io.appium.droiddriver.scroll.Direction.PhysicalDirection; 43import io.appium.droiddriver.util.Events; 44import io.appium.droiddriver.util.Logs; 45import io.appium.droiddriver.util.Strings; 46import io.appium.droiddriver.util.Strings.ToStringHelper; 47import io.appium.droiddriver.validators.Validator; 48 49/** 50 * Base UiElement that implements the common operations. 51 * 52 * @param <R> the type of the raw element this class wraps, for example, View or 53 * AccessibilityNodeInfo 54 * @param <E> the type of the concrete subclass of BaseUiElement 55 */ 56public abstract class BaseUiElement<R, E extends BaseUiElement<R, E>> implements UiElement { 57 // These two attribute names are used for debugging only. 58 // The two constants are used internally and must match to-uiautomator.xsl. 59 public static final String ATTRIB_VISIBLE_BOUNDS = "VisibleBounds"; 60 public static final String ATTRIB_NOT_VISIBLE = "NotVisible"; 61 62 private UiElementActor uiElementActor = EventUiElementActor.INSTANCE; 63 private Validator validator = null; 64 65 @SuppressWarnings("unchecked") 66 @Override 67 public <T> T get(Attribute attribute) { 68 return (T) getAttributes().get(attribute); 69 } 70 71 @Override 72 public String getText() { 73 return get(Attribute.TEXT); 74 } 75 76 @Override 77 public String getContentDescription() { 78 return get(Attribute.CONTENT_DESC); 79 } 80 81 @Override 82 public String getClassName() { 83 return get(Attribute.CLASS); 84 } 85 86 @Override 87 public String getResourceId() { 88 return get(Attribute.RESOURCE_ID); 89 } 90 91 @Override 92 public String getPackageName() { 93 return get(Attribute.PACKAGE); 94 } 95 96 @Override 97 public boolean isCheckable() { 98 return (Boolean) get(Attribute.CHECKABLE); 99 } 100 101 @Override 102 public boolean isChecked() { 103 return (Boolean) get(Attribute.CHECKED); 104 } 105 106 @Override 107 public boolean isClickable() { 108 return (Boolean) get(Attribute.CLICKABLE); 109 } 110 111 @Override 112 public boolean isEnabled() { 113 return (Boolean) get(Attribute.ENABLED); 114 } 115 116 @Override 117 public boolean isFocusable() { 118 return (Boolean) get(Attribute.FOCUSABLE); 119 } 120 121 @Override 122 public boolean isFocused() { 123 return (Boolean) get(Attribute.FOCUSED); 124 } 125 126 @Override 127 public boolean isScrollable() { 128 return (Boolean) get(Attribute.SCROLLABLE); 129 } 130 131 @Override 132 public boolean isLongClickable() { 133 return (Boolean) get(Attribute.LONG_CLICKABLE); 134 } 135 136 @Override 137 public boolean isPassword() { 138 return (Boolean) get(Attribute.PASSWORD); 139 } 140 141 @Override 142 public boolean isSelected() { 143 return (Boolean) get(Attribute.SELECTED); 144 } 145 146 @Override 147 public Rect getBounds() { 148 return get(Attribute.BOUNDS); 149 } 150 151 // TODO: expose these 3 methods in UiElement? 152 public int getSelectionStart() { 153 Integer value = get(Attribute.SELECTION_START); 154 return value == null ? 0 : value; 155 } 156 157 public int getSelectionEnd() { 158 Integer value = get(Attribute.SELECTION_END); 159 return value == null ? 0 : value; 160 } 161 162 public boolean hasSelection() { 163 final int selectionStart = getSelectionStart(); 164 final int selectionEnd = getSelectionEnd(); 165 166 return selectionStart >= 0 && selectionStart != selectionEnd; 167 } 168 169 @Override 170 public boolean perform(Action action) { 171 Logs.call(this, "perform", action); 172 if (validator != null && validator.isApplicable(this, action)) { 173 String failure = validator.validate(this, action); 174 if (failure != null) { 175 throw new DroidDriverException(toString() + " failed validation: " + failure); 176 } 177 } 178 179 // timeoutMillis <= 0 means no need to wait 180 if (action.getTimeoutMillis() <= 0) { 181 return doPerform(action); 182 } 183 return performAndWait(action); 184 } 185 186 protected boolean doPerform(Action action) { 187 return action.perform(this); 188 } 189 190 protected abstract void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis); 191 192 private boolean performAndWait(final Action action) { 193 FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() { 194 @Override 195 public Boolean call() { 196 return doPerform(action); 197 } 198 }); 199 doPerformAndWait(futureTask, action.getTimeoutMillis()); 200 201 try { 202 return futureTask.get(); 203 } catch (Throwable t) { 204 throw DroidDriverException.propagate(t); 205 } 206 } 207 208 @Override 209 public void setText(String text) { 210 Logs.call(this, "setText", text); 211 longClick(); // Gain focus; single click always activates IME. 212 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 213 clearText(); 214 } 215 216 if (TextUtils.isEmpty(text)) { 217 return; 218 } 219 220 perform(new TextAction(text)); 221 } 222 223 private void clearText() { 224 String text = getText(); 225 if (TextUtils.isEmpty(text)) { 226 return; 227 } 228 229 InputInjector injector = getInjector(); 230 SingleKeyAction.CTRL_MOVE_HOME.perform(injector, this); 231 232 final long shiftDownTime = Events.keyDown(injector, KeyEvent.KEYCODE_SHIFT_LEFT, 0); 233 SingleKeyAction.CTRL_MOVE_END.perform(injector, this); 234 Events.keyUp(injector, shiftDownTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0); 235 SingleKeyAction.DELETE.perform(injector, this); 236 } 237 238 @Override 239 public void click() { 240 uiElementActor.click(this); 241 } 242 243 @Override 244 public void longClick() { 245 uiElementActor.longClick(this); 246 } 247 248 @Override 249 public void doubleClick() { 250 uiElementActor.doubleClick(this); 251 } 252 253 @Override 254 public void scroll(PhysicalDirection direction) { 255 uiElementActor.scroll(this, direction); 256 } 257 258 protected abstract Map<Attribute, Object> getAttributes(); 259 260 protected abstract List<E> getChildren(); 261 262 @Override 263 public List<E> getChildren(Predicate<? super UiElement> predicate) { 264 List<E> children = getChildren(); 265 if (children == null) { 266 return Collections.emptyList(); 267 } 268 if (predicate == null || predicate.equals(Predicates.any())) { 269 return children; 270 } 271 272 List<E> filteredChildren = new ArrayList<E>(children.size()); 273 for (E child : children) { 274 if (predicate.apply(child)) { 275 filteredChildren.add(child); 276 } 277 } 278 return Collections.unmodifiableList(filteredChildren); 279 } 280 281 @Override 282 public String toString() { 283 ToStringHelper toStringHelper = Strings.toStringHelper(this); 284 for (Map.Entry<Attribute, Object> entry : getAttributes().entrySet()) { 285 addAttribute(toStringHelper, entry.getKey(), entry.getValue()); 286 } 287 if (!isVisible()) { 288 toStringHelper.addValue(ATTRIB_NOT_VISIBLE); 289 } else if (!getVisibleBounds().equals(getBounds())) { 290 toStringHelper.add(ATTRIB_VISIBLE_BOUNDS, getVisibleBounds().toShortString()); 291 } 292 return toStringHelper.toString(); 293 } 294 295 private static void addAttribute(ToStringHelper toStringHelper, Attribute attr, Object value) { 296 if (value != null) { 297 if (value instanceof Boolean) { 298 if ((Boolean) value) { 299 toStringHelper.addValue(attr.getName()); 300 } 301 } else if (value instanceof Rect) { 302 toStringHelper.add(attr.getName(), ((Rect) value).toShortString()); 303 } else { 304 toStringHelper.add(attr.getName(), value); 305 } 306 } 307 } 308 309 /** 310 * Gets the raw element used to create this UiElement. The attributes of this 311 * UiElement are based on a snapshot of the raw element at construction time. 312 * If the raw element is updated later, the attributes may not match. 313 */ 314 // TODO: expose in UiElement? 315 public abstract R getRawElement(); 316 317 public void setUiElementActor(UiElementActor uiElementActor) { 318 this.uiElementActor = uiElementActor; 319 } 320 321 /** 322 * Sets the validator to check when {@link #perform(Action)} is called. 323 */ 324 public void setValidator(Validator validator) { 325 this.validator = validator; 326 } 327} 328