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
17package io.appium.droiddriver.instrumentation;
18
19import static io.appium.droiddriver.util.Strings.charSequenceToString;
20
21import android.content.res.Resources;
22import android.graphics.Rect;
23import android.view.View;
24import android.view.ViewGroup;
25import android.widget.Checkable;
26import android.widget.TextView;
27import io.appium.droiddriver.actions.InputInjector;
28import io.appium.droiddriver.base.BaseUiElement;
29import io.appium.droiddriver.base.DroidDriverContext;
30import io.appium.droiddriver.finders.Attribute;
31import io.appium.droiddriver.util.InstrumentationUtils;
32import io.appium.droiddriver.util.Preconditions;
33import java.util.ArrayList;
34import java.util.Collections;
35import java.util.EnumMap;
36import java.util.List;
37import java.util.Map;
38import java.util.concurrent.Callable;
39import java.util.concurrent.FutureTask;
40
41/** A UiElement that is backed by a View. */
42public 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