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