/* * 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 com.google.android.droiddriver.uiautomation; import static com.google.android.droiddriver.util.Strings.charSequenceToString; import android.app.UiAutomation; import android.app.UiAutomation.AccessibilityEventFilter; import android.graphics.Rect; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import com.google.android.droiddriver.actions.InputInjector; import com.google.android.droiddriver.base.BaseUiElement; import com.google.android.droiddriver.finders.Attribute; import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable; import com.google.android.droiddriver.util.Preconditions; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeoutException; /** * A UiElement that gets attributes via the Accessibility API. */ public class UiAutomationElement extends BaseUiElement { private static final AccessibilityEventFilter ANY_EVENT_FILTER = new AccessibilityEventFilter() { @Override public boolean accept(AccessibilityEvent arg0) { return true; } }; private final AccessibilityNodeInfo node; private final UiAutomationContext context; private final Map attributes; private final boolean visible; private final Rect visibleBounds; private final UiAutomationElement parent; private final List children; /** * A snapshot of all attributes is taken at construction. The attributes of a * {@code UiAutomationElement} instance are immutable. If the underlying * {@link AccessibilityNodeInfo} is updated, a new {@code UiAutomationElement} * instance will be created in * {@link com.google.android.droiddriver.DroidDriver#refreshUiElementTree}. */ protected UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node, UiAutomationElement parent) { this.node = Preconditions.checkNotNull(node); this.context = Preconditions.checkNotNull(context); this.parent = parent; Map attribs = new EnumMap(Attribute.class); put(attribs, Attribute.PACKAGE, charSequenceToString(node.getPackageName())); put(attribs, Attribute.CLASS, charSequenceToString(node.getClassName())); put(attribs, Attribute.TEXT, charSequenceToString(node.getText())); put(attribs, Attribute.CONTENT_DESC, charSequenceToString(node.getContentDescription())); put(attribs, Attribute.RESOURCE_ID, charSequenceToString(node.getViewIdResourceName())); put(attribs, Attribute.CHECKABLE, node.isCheckable()); put(attribs, Attribute.CHECKED, node.isChecked()); put(attribs, Attribute.CLICKABLE, node.isClickable()); put(attribs, Attribute.ENABLED, node.isEnabled()); put(attribs, Attribute.FOCUSABLE, node.isFocusable()); put(attribs, Attribute.FOCUSED, node.isFocused()); put(attribs, Attribute.LONG_CLICKABLE, node.isLongClickable()); put(attribs, Attribute.PASSWORD, node.isPassword()); put(attribs, Attribute.SCROLLABLE, node.isScrollable()); if (node.getTextSelectionStart() >= 0 && node.getTextSelectionStart() != node.getTextSelectionEnd()) { attribs.put(Attribute.SELECTION_START, node.getTextSelectionStart()); attribs.put(Attribute.SELECTION_END, node.getTextSelectionEnd()); } put(attribs, Attribute.SELECTED, node.isSelected()); put(attribs, Attribute.BOUNDS, getBounds(node)); attributes = Collections.unmodifiableMap(attribs); // Order matters as getVisibleBounds depends on visible visible = node.isVisibleToUser(); visibleBounds = getVisibleBounds(node); List mutableChildren = buildChildren(node); this.children = mutableChildren == null ? null : Collections.unmodifiableList(mutableChildren); } private void put(Map attribs, Attribute key, Object value) { if (value != null) { attribs.put(key, value); } } private List buildChildren(AccessibilityNodeInfo node) { List children; int childCount = node.getChildCount(); if (childCount == 0) { children = null; } else { children = new ArrayList(childCount); for (int i = 0; i < childCount; i++) { AccessibilityNodeInfo child = node.getChild(i); if (child != null) { children.add(context.getElement(child, this)); } } } return children; } private Rect getBounds(AccessibilityNodeInfo node) { Rect rect = new Rect(); node.getBoundsInScreen(rect); return rect; } private Rect getVisibleBounds(AccessibilityNodeInfo node) { if (!visible) { return new Rect(); } Rect visibleBounds = getBounds(); UiAutomationElement parent = getParent(); Rect parentBounds; while (parent != null) { parentBounds = parent.getBounds(); visibleBounds.intersect(parentBounds); parent = parent.getParent(); } return visibleBounds; } @Override public Rect getVisibleBounds() { return visibleBounds; } @Override public boolean isVisible() { return visible; } @Override public UiAutomationElement getParent() { return parent; } @Override protected List getChildren() { return children; } @Override protected Map getAttributes() { return attributes; } @Override public InputInjector getInjector() { return context.getDriver().getInjector(); } /** * Note: This implementation of {@code doPerformAndWait} clears the * {@code AccessibilityEvent} queue. */ @Override protected void doPerformAndWait(final FutureTask futureTask, final long timeoutMillis) { context.callUiAutomation(new UiAutomationCallable() { @Override public Void call(UiAutomation uiAutomation) { try { uiAutomation.executeAndWaitForEvent(futureTask, ANY_EVENT_FILTER, timeoutMillis); } catch (TimeoutException e) { // This is for sync'ing with Accessibility API on best-effort because // it is not reliable. // Exception is ignored here. Tests will fail anyways if this is // critical. // Actions should usually trigger some AccessibilityEvent's, but some // widgets fail to do so, resulting in stale AccessibilityNodeInfo's. // As a work-around, force to clear the AccessibilityNodeInfoCache. // A legitimate case of no AccessibilityEvent is when scrolling has // reached the end, but we cannot tell whether it's legitimate or the // widget has bugs, so clearAccessibilityNodeInfoCache anyways. context.getDriver().clearAccessibilityNodeInfoCache(); } return null; } }); } @Override public AccessibilityNodeInfo getRawElement() { return node; } }