/*
* Copyright (C) 2013 DroidDriver committers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.appium.droiddriver.base;
import android.graphics.Rect;
import android.os.Build;
import android.text.TextUtils;
import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import io.appium.droiddriver.UiElement;
import io.appium.droiddriver.actions.Action;
import io.appium.droiddriver.actions.EventUiElementActor;
import io.appium.droiddriver.actions.InputInjector;
import io.appium.droiddriver.actions.SingleKeyAction;
import io.appium.droiddriver.actions.TextAction;
import io.appium.droiddriver.actions.UiElementActor;
import io.appium.droiddriver.exceptions.DroidDriverException;
import io.appium.droiddriver.finders.Attribute;
import io.appium.droiddriver.finders.Predicate;
import io.appium.droiddriver.finders.Predicates;
import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
import io.appium.droiddriver.util.Events;
import io.appium.droiddriver.util.Logs;
import io.appium.droiddriver.util.Strings;
import io.appium.droiddriver.util.Strings.ToStringHelper;
import io.appium.droiddriver.validators.Validator;
/**
* Base UiElement that implements the common operations.
*
* @param <R> the type of the raw element this class wraps, for example, View or
* AccessibilityNodeInfo
* @param <E> the type of the concrete subclass of BaseUiElement
*/
public abstract class BaseUiElement<R, E extends BaseUiElement<R, E>> implements UiElement {
// These two attribute names are used for debugging only.
// The two constants are used internally and must match to-uiautomator.xsl.
public static final String ATTRIB_VISIBLE_BOUNDS = "VisibleBounds";
public static final String ATTRIB_NOT_VISIBLE = "NotVisible";
private UiElementActor uiElementActor = EventUiElementActor.INSTANCE;
private Validator validator = null;
@SuppressWarnings("unchecked")
@Override
public <T> T get(Attribute attribute) {
return (T) getAttributes().get(attribute);
}
@Override
public String getText() {
return get(Attribute.TEXT);
}
@Override
public String getContentDescription() {
return get(Attribute.CONTENT_DESC);
}
@Override
public String getClassName() {
return get(Attribute.CLASS);
}
@Override
public String getResourceId() {
return get(Attribute.RESOURCE_ID);
}
@Override
public String getPackageName() {
return get(Attribute.PACKAGE);
}
@Override
public boolean isCheckable() {
return (Boolean) get(Attribute.CHECKABLE);
}
@Override
public boolean isChecked() {
return (Boolean) get(Attribute.CHECKED);
}
@Override
public boolean isClickable() {
return (Boolean) get(Attribute.CLICKABLE);
}
@Override
public boolean isEnabled() {
return (Boolean) get(Attribute.ENABLED);
}
@Override
public boolean isFocusable() {
return (Boolean) get(Attribute.FOCUSABLE);
}
@Override
public boolean isFocused() {
return (Boolean) get(Attribute.FOCUSED);
}
@Override
public boolean isScrollable() {
return (Boolean) get(Attribute.SCROLLABLE);
}
@Override
public boolean isLongClickable() {
return (Boolean) get(Attribute.LONG_CLICKABLE);
}
@Override
public boolean isPassword() {
return (Boolean) get(Attribute.PASSWORD);
}
@Override
public boolean isSelected() {
return (Boolean) get(Attribute.SELECTED);
}
@Override
public Rect getBounds() {
return get(Attribute.BOUNDS);
}
// TODO: expose these 3 methods in UiElement?
public int getSelectionStart() {
Integer value = get(Attribute.SELECTION_START);
return value == null ? 0 : value;
}
public int getSelectionEnd() {
Integer value = get(Attribute.SELECTION_END);
return value == null ? 0 : value;
}
public boolean hasSelection() {
final int selectionStart = getSelectionStart();
final int selectionEnd = getSelectionEnd();
return selectionStart >= 0 && selectionStart != selectionEnd;
}
@Override
public boolean perform(Action action) {
Logs.call(this, "perform", action);
if (validator != null && validator.isApplicable(this, action)) {
String failure = validator.validate(this, action);
if (failure != null) {
throw new DroidDriverException(toString() + " failed validation: " + failure);
}
}
// timeoutMillis <= 0 means no need to wait
if (action.getTimeoutMillis() <= 0) {
return doPerform(action);
}
return performAndWait(action);
}
protected boolean doPerform(Action action) {
return action.perform(this);
}
protected abstract void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis);
private boolean performAndWait(final Action action) {
FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() {
@Override
public Boolean call() {
return doPerform(action);
}
});
doPerformAndWait(futureTask, action.getTimeoutMillis());
try {
return futureTask.get();
} catch (Throwable t) {
throw DroidDriverException.propagate(t);
}
}
@Override
public void setText(String text) {
Logs.call(this, "setText", text);
longClick(); // Gain focus; single click always activates IME.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
clearText();
}
if (TextUtils.isEmpty(text)) {
return;
}
perform(new TextAction(text));
}
private void clearText() {
String text = getText();
if (TextUtils.isEmpty(text)) {
return;
}
InputInjector injector = getInjector();
SingleKeyAction.CTRL_MOVE_HOME.perform(injector, this);
final long shiftDownTime = Events.keyDown(injector, KeyEvent.KEYCODE_SHIFT_LEFT, 0);
SingleKeyAction.CTRL_MOVE_END.perform(injector, this);
Events.keyUp(injector, shiftDownTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0);
SingleKeyAction.DELETE.perform(injector, this);
}
@Override
public void click() {
uiElementActor.click(this);
}
@Override
public void longClick() {
uiElementActor.longClick(this);
}
@Override
public void doubleClick() {
uiElementActor.doubleClick(this);
}
@Override
public void scroll(PhysicalDirection direction) {
uiElementActor.scroll(this, direction);
}
protected abstract Map<Attribute, Object> getAttributes();
protected abstract List<E> getChildren();
@Override
public List<E> getChildren(Predicate<? super UiElement> predicate) {
List<E> children = getChildren();
if (children == null) {
return Collections.emptyList();
}
if (predicate == null || predicate.equals(Predicates.any())) {
return children;
}
List<E> filteredChildren = new ArrayList<E>(children.size());
for (E child : children) {
if (predicate.apply(child)) {
filteredChildren.add(child);
}
}
return Collections.unmodifiableList(filteredChildren);
}
@Override
public String toString() {
ToStringHelper toStringHelper = Strings.toStringHelper(this);
for (Map.Entry<Attribute, Object> entry : getAttributes().entrySet()) {
addAttribute(toStringHelper, entry.getKey(), entry.getValue());
}
if (!isVisible()) {
toStringHelper.addValue(ATTRIB_NOT_VISIBLE);
} else if (!getVisibleBounds().equals(getBounds())) {
toStringHelper.add(ATTRIB_VISIBLE_BOUNDS, getVisibleBounds().toShortString());
}
return toStringHelper.toString();
}
private static void addAttribute(ToStringHelper toStringHelper, Attribute attr, Object value) {
if (value != null) {
if (value instanceof Boolean) {
if ((Boolean) value) {
toStringHelper.addValue(attr.getName());
}
} else if (value instanceof Rect) {
toStringHelper.add(attr.getName(), ((Rect) value).toShortString());
} else {
toStringHelper.add(attr.getName(), value);
}
}
}
/**
* Gets the raw element used to create this UiElement. The attributes of this
* UiElement are based on a snapshot of the raw element at construction time.
* If the raw element is updated later, the attributes may not match.
*/
// TODO: expose in UiElement?
public abstract R getRawElement();
public void setUiElementActor(UiElementActor uiElementActor) {
this.uiElementActor = uiElementActor;
}
/**
* Sets the validator to check when {@link #perform(Action)} is called.
*/
public void setValidator(Validator validator) {
this.validator = validator;
}
}