/* * 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; } }