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