• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    */
ViewElement(DroidDriverContext<View, ViewElement> context, View view, ViewElement parent)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
getVisibleBounds()78   public Rect getVisibleBounds() {
79     return visibleBounds;
80   }
81 
82   @Override
isVisible()83   public boolean isVisible() {
84     return visible;
85   }
86 
87   @Override
getParent()88   public ViewElement getParent() {
89     return parent;
90   }
91 
92   @Override
getChildren()93   protected List<ViewElement> getChildren() {
94     return children;
95   }
96 
97   @Override
getAttributes()98   protected Map<Attribute, Object> getAttributes() {
99     return attributes;
100   }
101 
102   @Override
getInjector()103   public InputInjector getInjector() {
104     return context.getDriver().getInjector();
105   }
106 
107   @Override
doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis)108   protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) {
109     futureTask.run();
110     InstrumentationUtils.tryWaitForIdleSync(timeoutMillis);
111   }
112 
113   @Override
getRawElement()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 
AttributesSnapshot(View view)125     private AttributesSnapshot(View view) {
126       this.view = view;
127     }
128 
129     @Override
call()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 
put(Attribute key, Object value)165     private void put(Attribute key, Object value) {
166       if (value != null) {
167         attribs.put(key, value);
168       }
169     }
170 
getText()171     private String getText() {
172       if (!(view instanceof TextView)) {
173         return null;
174       }
175       return charSequenceToString(((TextView) view).getText());
176     }
177 
getClassName()178     private String getClassName() {
179       return view.getClass().getName();
180     }
181 
getResourceId()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 
isChecked()193     private boolean isChecked() {
194       return view instanceof Checkable && ((Checkable) view).isChecked();
195     }
196 
isScrollable()197     private boolean isScrollable() {
198       // TODO: find a meaningful implementation
199       return true;
200     }
201 
isPassword()202     private boolean isPassword() {
203       // TODO: find a meaningful implementation
204       return false;
205     }
206 
getBounds()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 
getVisibleBounds()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 
setChildViews()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