Home | History | Annotate | Download | only in base
      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 
     17 package io.appium.droiddriver.base;
     18 
     19 import android.graphics.Rect;
     20 import android.os.Build;
     21 import android.text.TextUtils;
     22 import android.view.KeyEvent;
     23 
     24 import java.util.ArrayList;
     25 import java.util.Collections;
     26 import java.util.List;
     27 import java.util.Map;
     28 import java.util.concurrent.Callable;
     29 import java.util.concurrent.FutureTask;
     30 
     31 import io.appium.droiddriver.UiElement;
     32 import io.appium.droiddriver.actions.Action;
     33 import io.appium.droiddriver.actions.EventUiElementActor;
     34 import io.appium.droiddriver.actions.InputInjector;
     35 import io.appium.droiddriver.actions.SingleKeyAction;
     36 import io.appium.droiddriver.actions.TextAction;
     37 import io.appium.droiddriver.actions.UiElementActor;
     38 import io.appium.droiddriver.exceptions.DroidDriverException;
     39 import io.appium.droiddriver.finders.Attribute;
     40 import io.appium.droiddriver.finders.Predicate;
     41 import io.appium.droiddriver.finders.Predicates;
     42 import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
     43 import io.appium.droiddriver.util.Events;
     44 import io.appium.droiddriver.util.Logs;
     45 import io.appium.droiddriver.util.Strings;
     46 import io.appium.droiddriver.util.Strings.ToStringHelper;
     47 import 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  */
     56 public 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