diff options
Diffstat (limited to 'src/io/appium/droiddriver/instrumentation/ViewElement.java')
-rw-r--r-- | src/io/appium/droiddriver/instrumentation/ViewElement.java | 286 |
1 files changed, 286 insertions, 0 deletions
diff --git a/src/io/appium/droiddriver/instrumentation/ViewElement.java b/src/io/appium/droiddriver/instrumentation/ViewElement.java new file mode 100644 index 0000000..a92dee4 --- /dev/null +++ b/src/io/appium/droiddriver/instrumentation/ViewElement.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2013 DroidDriver committers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.droiddriver.instrumentation; + +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Checkable; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.FutureTask; + +import io.appium.droiddriver.actions.InputInjector; +import io.appium.droiddriver.base.BaseUiElement; +import io.appium.droiddriver.base.DroidDriverContext; +import io.appium.droiddriver.exceptions.DroidDriverException; +import io.appium.droiddriver.finders.Attribute; +import io.appium.droiddriver.util.Preconditions; + +import static io.appium.droiddriver.util.Strings.charSequenceToString; + +/** + * A UiElement that is backed by a View. + */ +public class ViewElement extends BaseUiElement<View, ViewElement> { + private static class SnapshotViewAttributesRunnable implements Runnable { + private final View view; + final Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(Attribute.class); + boolean visible; + Rect visibleBounds; + List<View> childViews; + Throwable exception; + + private SnapshotViewAttributesRunnable(View view) { + this.view = view; + } + + @Override + public void run() { + try { + put(Attribute.PACKAGE, view.getContext().getPackageName()); + put(Attribute.CLASS, getClassName()); + put(Attribute.TEXT, getText()); + put(Attribute.CONTENT_DESC, charSequenceToString(view.getContentDescription())); + put(Attribute.RESOURCE_ID, getResourceId()); + put(Attribute.CHECKABLE, view instanceof Checkable); + put(Attribute.CHECKED, isChecked()); + put(Attribute.CLICKABLE, view.isClickable()); + put(Attribute.ENABLED, view.isEnabled()); + put(Attribute.FOCUSABLE, view.isFocusable()); + put(Attribute.FOCUSED, view.isFocused()); + put(Attribute.LONG_CLICKABLE, view.isLongClickable()); + put(Attribute.PASSWORD, isPassword()); + put(Attribute.SCROLLABLE, isScrollable()); + if (view instanceof TextView) { + TextView textView = (TextView) view; + if (textView.hasSelection()) { + attribs.put(Attribute.SELECTION_START, textView.getSelectionStart()); + attribs.put(Attribute.SELECTION_END, textView.getSelectionEnd()); + } + } + put(Attribute.SELECTED, view.isSelected()); + put(Attribute.BOUNDS, getBounds()); + + // Order matters as setVisible() depends on setVisibleBounds(). + this.visibleBounds = getVisibleBounds(); + // isShown() checks the visibility flag of this view and ancestors; it + // needs to have the VISIBLE flag as well as non-empty bounds to be + // visible. + this.visible = view.isShown() && !visibleBounds.isEmpty(); + setChildViews(); + } catch (Throwable e) { + exception = e; + } + } + + private void put(Attribute key, Object value) { + if (value != null) { + attribs.put(key, value); + } + } + + private String getText() { + if (!(view instanceof TextView)) { + return null; + } + return charSequenceToString(((TextView) view).getText()); + } + + private String getClassName() { + String className = view.getClass().getName(); + return CLASS_NAME_OVERRIDES.containsKey(className) ? CLASS_NAME_OVERRIDES.get(className) + : className; + } + + private String getResourceId() { + if (view.getId() != View.NO_ID && view.getResources() != null) { + try { + return charSequenceToString(view.getResources().getResourceName(view.getId())); + } catch (Resources.NotFoundException nfe) { + /* ignore */ + } + } + return null; + } + + private boolean isChecked() { + return view instanceof Checkable && ((Checkable) view).isChecked(); + } + + private boolean isScrollable() { + // TODO: find a meaningful implementation + return true; + } + + private boolean isPassword() { + // TODO: find a meaningful implementation + return false; + } + + private Rect getBounds() { + Rect rect = new Rect(); + int[] xy = new int[2]; + view.getLocationOnScreen(xy); + rect.set(xy[0], xy[1], xy[0] + view.getWidth(), xy[1] + view.getHeight()); + return rect; + } + + private Rect getVisibleBounds() { + Rect visibleBounds = new Rect(); + if (!view.isShown() || !view.getGlobalVisibleRect(visibleBounds)) { + visibleBounds.setEmpty(); + } + int[] xyScreen = new int[2]; + view.getLocationOnScreen(xyScreen); + int[] xyWindow = new int[2]; + view.getLocationInWindow(xyWindow); + int windowLeft = xyScreen[0] - xyWindow[0]; + int windowTop = xyScreen[1] - xyWindow[1]; + + // Bounds are relative to root view; adjust to screen coordinates. + visibleBounds.offset(windowLeft, windowTop); + return visibleBounds; + } + + private void setChildViews() { + if (!(view instanceof ViewGroup)) { + return; + } + ViewGroup group = (ViewGroup) view; + int childCount = group.getChildCount(); + childViews = new ArrayList<View>(childCount); + for (int i = 0; i < childCount; i++) { + View child = group.getChildAt(i); + if (child != null) { + childViews.add(child); + } + } + } + } + + private static final Map<String, String> CLASS_NAME_OVERRIDES = new HashMap<String, String>(); + + /** + * Typically users find the class name to use in tests using SDK tool + * uiautomatorviewer. This name is returned by + * {@link AccessibilityNodeInfo#getClassName}. If the app uses custom View + * classes that do not call {@link AccessibilityNodeInfo#setClassName} with + * the actual class name, different types of drivers see different class names + * (InstrumentationDriver sees the actual class name, while UiAutomationDriver + * sees {@link AccessibilityNodeInfo#getClassName}). + * <p> + * If tests fail with InstrumentationDriver, find the actual class name by + * examining app code or by calling + * {@link io.appium.droiddriver.DroidDriver#dumpUiElementTree}, then + * call this method in setUp to override it with the class name seen in + * uiautomatorviewer. + * </p> + * A better solution is to use resource-id instead of classname, which is an + * implementation detail and subject to change. + */ + public static void overrideClassName(String actualClassName, String overridingClassName) { + CLASS_NAME_OVERRIDES.put(actualClassName, overridingClassName); + } + + private final DroidDriverContext<View, ViewElement> context; + private final View view; + private final Map<Attribute, Object> attributes; + private final boolean visible; + private final Rect visibleBounds; + private final ViewElement parent; + private final List<ViewElement> children; + + /** + * A snapshot of all attributes is taken at construction. The attributes of a + * {@code ViewElement} instance are immutable. If the underlying view is + * updated, a new {@code ViewElement} instance will be created in + * {@link io.appium.droiddriver.DroidDriver#refreshUiElementTree}. + */ + public ViewElement(DroidDriverContext<View, ViewElement> context, View view, ViewElement parent) { + this.context = Preconditions.checkNotNull(context); + this.view = Preconditions.checkNotNull(view); + this.parent = parent; + SnapshotViewAttributesRunnable attributesSnapshot = new SnapshotViewAttributesRunnable(view); + context.runOnMainSync(attributesSnapshot); + if (attributesSnapshot.exception != null) { + throw new DroidDriverException(attributesSnapshot.exception); + } + + attributes = Collections.unmodifiableMap(attributesSnapshot.attribs); + this.visibleBounds = attributesSnapshot.visibleBounds; + this.visible = attributesSnapshot.visible; + if (attributesSnapshot.childViews == null) { + this.children = null; + } else { + List<ViewElement> children = new ArrayList<ViewElement>(attributesSnapshot.childViews.size()); + for (View childView : attributesSnapshot.childViews) { + children.add(context.getElement(childView, this)); + } + this.children = Collections.unmodifiableList(children); + } + } + + @Override + public Rect getVisibleBounds() { + return visibleBounds; + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public ViewElement getParent() { + return parent; + } + + @Override + protected List<ViewElement> getChildren() { + return children; + } + + @Override + protected Map<Attribute, Object> getAttributes() { + return attributes; + } + + @Override + public InputInjector getInjector() { + return context.getDriver().getInjector(); + } + + @Override + protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) { + futureTask.run(); + context.tryWaitForIdleSync(timeoutMillis); + } + + @Override + public View getRawElement() { + return view; + } +} |