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 com.google.android.droiddriver.InputInjector;
20import com.google.android.droiddriver.UiElement;
21import com.google.android.droiddriver.actions.Action;
22import com.google.android.droiddriver.actions.ClickAction;
23import com.google.android.droiddriver.actions.ScrollDirection;
24import com.google.android.droiddriver.actions.SwipeAction;
25import com.google.android.droiddriver.actions.TypeAction;
26import com.google.android.droiddriver.exceptions.ElementNotVisibleException;
27import com.google.android.droiddriver.finders.Attribute;
28import com.google.android.droiddriver.finders.ByXPath;
29import com.google.android.droiddriver.util.Logs;
30import com.google.common.base.Objects;
31import com.google.common.base.Objects.ToStringHelper;
32import com.google.common.base.Predicate;
33import com.google.common.base.Predicates;
34import com.google.common.collect.ImmutableList;
35
36import org.w3c.dom.Element;
37
38import java.lang.ref.WeakReference;
39import java.util.List;
40import java.util.concurrent.Callable;
41import java.util.concurrent.FutureTask;
42
43/**
44 * Abstract implementation with common methods already implemented.
45 */
46public abstract class AbstractUiElement implements UiElement {
47  private WeakReference<Element> domNode;
48
49  @Override
50  public <T> T get(Attribute attribute) {
51    return attribute.getValue(this);
52  }
53
54  @Override
55  public boolean perform(Action action) {
56    Logs.call(this, "perform", action);
57    checkVisible();
58    return performAndWait(action);
59  }
60
61  protected boolean doPerform(Action action) {
62    return action.perform(getInjector(), this);
63  }
64
65  protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) {
66    // ignores timeoutMillis; subclasses can override this behavior
67    futureTask.run();
68  }
69
70  private boolean performAndWait(final Action action) {
71    // timeoutMillis <= 0 means no need to wait
72    if (action.getTimeoutMillis() <= 0) {
73      return doPerform(action);
74    }
75
76    FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() {
77      @Override
78      public Boolean call() {
79        return doPerform(action);
80      }
81    });
82    doPerformAndWait(futureTask, action.getTimeoutMillis());
83    try {
84      return futureTask.get();
85    } catch (Exception e) {
86      // should not reach here b/c futureTask has run
87      return false;
88    }
89  }
90
91  @Override
92  public void setText(String text) {
93    // TODO: Define common actions as a const.
94    perform(new TypeAction(text));
95    // TypeAction may not be effective immediately and reflected bygetText(),
96    // so the following will fail.
97    // if (Logs.DEBUG) {
98    // String actual = getText();
99    // if (!text.equals(actual)) {
100    // throw new DroidDriverException(String.format(
101    // "setText failed: expected=\"%s\", actual=\"%s\"", text, actual));
102    // }
103    // }
104  }
105
106  @Override
107  public void click() {
108    perform(ClickAction.SINGLE);
109  }
110
111  @Override
112  public void longClick() {
113    perform(ClickAction.LONG);
114  }
115
116  @Override
117  public void doubleClick() {
118    perform(ClickAction.DOUBLE);
119  }
120
121  @Override
122  public void scroll(ScrollDirection direction) {
123    perform(new SwipeAction(direction, false));
124  }
125
126  @Override
127  public abstract AbstractUiElement getChild(int index);
128
129  protected abstract InputInjector getInjector();
130
131  private void checkVisible() {
132    if (!isVisible()) {
133      throw new ElementNotVisibleException(this);
134    }
135  }
136
137  @Override
138  public List<UiElement> getChildren(Predicate<? super UiElement> predicate) {
139    predicate = Predicates.and(Predicates.notNull(), predicate);
140    // TODO: Use internal data when we take snapshot of current node tree.
141    ImmutableList.Builder<UiElement> builder = ImmutableList.builder();
142    for (int i = 0; i < getChildCount(); i++) {
143      UiElement child = getChild(i);
144      if (predicate.apply(child)) {
145        builder.add(child);
146      }
147    }
148    return builder.build();
149  }
150
151  @Override
152  public String toString() {
153    ToStringHelper toStringHelper = Objects.toStringHelper(this);
154    for (Attribute attr : Attribute.values()) {
155      addAttribute(toStringHelper, attr, get(attr));
156    }
157    return toStringHelper.toString();
158  }
159
160  private static void addAttribute(ToStringHelper toStringHelper, Attribute attr, Object value) {
161    if (value != null) {
162      if (value instanceof Boolean) {
163        if ((Boolean) value) {
164          toStringHelper.addValue(attr.getName());
165        }
166      } else {
167        toStringHelper.add(attr.getName(), value);
168      }
169    }
170  }
171
172  /**
173   * Used internally in {@link ByXPath}. Returns the DOM node representing this
174   * UiElement. The DOM is constructed from the UiElement tree.
175   * <p>
176   * TODO: move this to {@link ByXPath}. This requires a BiMap using
177   * WeakReference for both keys and values, which is error-prone. This will be
178   * deferred until we decide whether to clear cache upon getRootElement.
179   */
180  public Element getDomNode() {
181    if (domNode == null || domNode.get() == null) {
182      domNode = new WeakReference<Element>(ByXPath.buildDomNode(this));
183    }
184    return domNode.get();
185  }
186}
187