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