Home | History | Annotate | Download | only in instrumentation
      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.instrumentation;
     18 
     19 import static io.appium.droiddriver.util.Strings.charSequenceToString;
     20 
     21 import android.content.res.Resources;
     22 import android.graphics.Rect;
     23 import android.view.View;
     24 import android.view.ViewGroup;
     25 import android.widget.Checkable;
     26 import android.widget.TextView;
     27 import io.appium.droiddriver.actions.InputInjector;
     28 import io.appium.droiddriver.base.BaseUiElement;
     29 import io.appium.droiddriver.base.DroidDriverContext;
     30 import io.appium.droiddriver.finders.Attribute;
     31 import io.appium.droiddriver.util.InstrumentationUtils;
     32 import io.appium.droiddriver.util.Preconditions;
     33 import java.util.ArrayList;
     34 import java.util.Collections;
     35 import java.util.EnumMap;
     36 import java.util.List;
     37 import java.util.Map;
     38 import java.util.concurrent.Callable;
     39 import java.util.concurrent.FutureTask;
     40 
     41 /** A UiElement that is backed by a View. */
     42 public class ViewElement extends BaseUiElement<View, ViewElement> {
     43   private final DroidDriverContext<View, ViewElement> context;
     44   private final View view;
     45   private final Map<Attribute, Object> attributes;
     46   private final boolean visible;
     47   private final Rect visibleBounds;
     48   private final ViewElement parent;
     49   private final List<ViewElement> children;
     50 
     51   /**
     52    * A snapshot of all attributes is taken at construction. The attributes of a {@code ViewElement}
     53    * instance are immutable. If the underlying view is updated, a new {@code ViewElement} instance
     54    * will be created in {@link io.appium.droiddriver.DroidDriver#refreshUiElementTree}.
     55    */
     56   public ViewElement(DroidDriverContext<View, ViewElement> context, View view, ViewElement parent) {
     57     this.context = Preconditions.checkNotNull(context);
     58     this.view = Preconditions.checkNotNull(view);
     59     this.parent = parent;
     60     AttributesSnapshot attributesSnapshot = new AttributesSnapshot(view);
     61     InstrumentationUtils.runOnMainSyncWithTimeout(attributesSnapshot);
     62 
     63     attributes = Collections.unmodifiableMap(attributesSnapshot.attribs);
     64     this.visibleBounds = attributesSnapshot.visibleBounds;
     65     this.visible = attributesSnapshot.visible;
     66     if (attributesSnapshot.childViews == null) {
     67       this.children = null;
     68     } else {
     69       List<ViewElement> children = new ArrayList<>(attributesSnapshot.childViews.size());
     70       for (View childView : attributesSnapshot.childViews) {
     71         children.add(context.getElement(childView, this));
     72       }
     73       this.children = Collections.unmodifiableList(children);
     74     }
     75   }
     76 
     77   @Override
     78   public Rect getVisibleBounds() {
     79     return visibleBounds;
     80   }
     81 
     82   @Override
     83   public boolean isVisible() {
     84     return visible;
     85   }
     86 
     87   @Override
     88   public ViewElement getParent() {
     89     return parent;
     90   }
     91 
     92   @Override
     93   protected List<ViewElement> getChildren() {
     94     return children;
     95   }
     96 
     97   @Override
     98   protected Map<Attribute, Object> getAttributes() {
     99     return attributes;
    100   }
    101 
    102   @Override
    103   public InputInjector getInjector() {
    104     return context.getDriver().getInjector();
    105   }
    106 
    107   @Override
    108   protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) {
    109     futureTask.run();
    110     InstrumentationUtils.tryWaitForIdleSync(timeoutMillis);
    111   }
    112 
    113   @Override
    114   public View getRawElement() {
    115     return view;
    116   }
    117 
    118   private static class AttributesSnapshot implements Callable<Void> {
    119     final Map<Attribute, Object> attribs = new EnumMap<>(Attribute.class);
    120     private final View view;
    121     boolean visible;
    122     Rect visibleBounds;
    123     List<View> childViews;
    124 
    125     private AttributesSnapshot(View view) {
    126       this.view = view;
    127     }
    128 
    129     @Override
    130     public Void call() {
    131       put(Attribute.PACKAGE, view.getContext().getPackageName());
    132       put(Attribute.CLASS, getClassName());
    133       put(Attribute.TEXT, getText());
    134       put(Attribute.CONTENT_DESC, charSequenceToString(view.getContentDescription()));
    135       put(Attribute.RESOURCE_ID, getResourceId());
    136       put(Attribute.CHECKABLE, view instanceof Checkable);
    137       put(Attribute.CHECKED, isChecked());
    138       put(Attribute.CLICKABLE, view.isClickable());
    139       put(Attribute.ENABLED, view.isEnabled());
    140       put(Attribute.FOCUSABLE, view.isFocusable());
    141       put(Attribute.FOCUSED, view.isFocused());
    142       put(Attribute.LONG_CLICKABLE, view.isLongClickable());
    143       put(Attribute.PASSWORD, isPassword());
    144       put(Attribute.SCROLLABLE, isScrollable());
    145       if (view instanceof TextView) {
    146         TextView textView = (TextView) view;
    147         if (textView.hasSelection()) {
    148           attribs.put(Attribute.SELECTION_START, textView.getSelectionStart());
    149           attribs.put(Attribute.SELECTION_END, textView.getSelectionEnd());
    150         }
    151       }
    152       put(Attribute.SELECTED, view.isSelected());
    153       put(Attribute.BOUNDS, getBounds());
    154 
    155       // Order matters as setVisible() depends on setVisibleBounds().
    156       this.visibleBounds = getVisibleBounds();
    157       // isShown() checks the visibility flag of this view and ancestors; it
    158       // needs to have the VISIBLE flag as well as non-empty bounds to be
    159       // visible.
    160       this.visible = view.isShown() && !visibleBounds.isEmpty();
    161       setChildViews();
    162       return null;
    163     }
    164 
    165     private void put(Attribute key, Object value) {
    166       if (value != null) {
    167         attribs.put(key, value);
    168       }
    169     }
    170 
    171     private String getText() {
    172       if (!(view instanceof TextView)) {
    173         return null;
    174       }
    175       return charSequenceToString(((TextView) view).getText());
    176     }
    177 
    178     private String getClassName() {
    179       return view.getClass().getName();
    180     }
    181 
    182     private String getResourceId() {
    183       if (view.getId() != View.NO_ID && view.getResources() != null) {
    184         try {
    185           return charSequenceToString(view.getResources().getResourceName(view.getId()));
    186         } catch (Resources.NotFoundException nfe) {
    187           /* ignore */
    188         }
    189       }
    190       return null;
    191     }
    192 
    193     private boolean isChecked() {
    194       return view instanceof Checkable && ((Checkable) view).isChecked();
    195     }
    196 
    197     private boolean isScrollable() {
    198       // TODO: find a meaningful implementation
    199       return true;
    200     }
    201 
    202     private boolean isPassword() {
    203       // TODO: find a meaningful implementation
    204       return false;
    205     }
    206 
    207     private Rect getBounds() {
    208       Rect rect = new Rect();
    209       int[] xy = new int[2];
    210       view.getLocationOnScreen(xy);
    211       rect.set(xy[0], xy[1], xy[0] + view.getWidth(), xy[1] + view.getHeight());
    212       return rect;
    213     }
    214 
    215     private Rect getVisibleBounds() {
    216       Rect visibleBounds = new Rect();
    217       if (!view.isShown() || !view.getGlobalVisibleRect(visibleBounds)) {
    218         visibleBounds.setEmpty();
    219       }
    220       int[] xyScreen = new int[2];
    221       view.getLocationOnScreen(xyScreen);
    222       int[] xyWindow = new int[2];
    223       view.getLocationInWindow(xyWindow);
    224       int windowLeft = xyScreen[0] - xyWindow[0];
    225       int windowTop = xyScreen[1] - xyWindow[1];
    226 
    227       // Bounds are relative to root view; adjust to screen coordinates.
    228       visibleBounds.offset(windowLeft, windowTop);
    229       return visibleBounds;
    230     }
    231 
    232     private void setChildViews() {
    233       if (!(view instanceof ViewGroup)) {
    234         return;
    235       }
    236       ViewGroup group = (ViewGroup) view;
    237       int childCount = group.getChildCount();
    238       childViews = new ArrayList<>(childCount);
    239       for (int i = 0; i < childCount; i++) {
    240         View child = group.getChildAt(i);
    241         if (child != null) {
    242           childViews.add(child);
    243         }
    244       }
    245     }
    246   }
    247 }
    248