| /* |
| * 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.uiautomation; |
| |
| import static io.appium.droiddriver.util.Strings.charSequenceToString; |
| |
| import android.annotation.TargetApi; |
| import android.app.UiAutomation; |
| import android.app.UiAutomation.AccessibilityEventFilter; |
| import android.graphics.Rect; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| |
| 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; |
| |
| import io.appium.droiddriver.actions.InputInjector; |
| import io.appium.droiddriver.base.BaseUiElement; |
| import io.appium.droiddriver.finders.Attribute; |
| import io.appium.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable; |
| import io.appium.droiddriver.util.Preconditions; |
| |
| /** |
| * A UiElement that gets attributes via the Accessibility API. |
| */ |
| @TargetApi(18) |
| public class UiAutomationElement extends BaseUiElement<AccessibilityNodeInfo, UiAutomationElement> { |
| 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<Attribute, Object> attributes; |
| private final boolean visible; |
| private final Rect visibleBounds; |
| private final UiAutomationElement parent; |
| private final List<UiAutomationElement> 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 io.appium.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<Attribute, Object> attribs = new EnumMap<Attribute, Object>(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 findVisibleBounds depends on visible |
| visible = node.isVisibleToUser(); |
| visibleBounds = findVisibleBounds(); |
| List<UiAutomationElement> mutableChildren = buildChildren(node); |
| this.children = mutableChildren == null ? null : Collections.unmodifiableList(mutableChildren); |
| } |
| |
| private void put(Map<Attribute, Object> attribs, Attribute key, Object value) { |
| if (value != null) { |
| attribs.put(key, value); |
| } |
| } |
| |
| private List<UiAutomationElement> buildChildren(AccessibilityNodeInfo node) { |
| List<UiAutomationElement> children; |
| int childCount = node.getChildCount(); |
| if (childCount == 0) { |
| children = null; |
| } else { |
| children = new ArrayList<UiAutomationElement>(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 findVisibleBounds() { |
| if (!visible) { |
| return new Rect(); |
| } |
| Rect foundBounds = getBounds(); |
| UiAutomationElement parent = getParent(); |
| while (parent != null) { |
| if (!foundBounds.intersect(parent.getBounds())) { |
| return new Rect(); |
| } |
| parent = parent.getParent(); |
| } |
| return foundBounds; |
| } |
| |
| @Override |
| public Rect getVisibleBounds() { |
| return visibleBounds; |
| } |
| |
| @Override |
| public boolean isVisible() { |
| return visible; |
| } |
| |
| @Override |
| public UiAutomationElement getParent() { |
| return parent; |
| } |
| |
| @Override |
| protected List<UiAutomationElement> getChildren() { |
| return children; |
| } |
| |
| @Override |
| protected Map<Attribute, Object> 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<Boolean> futureTask, final long timeoutMillis) { |
| context.callUiAutomation(new UiAutomationCallable<Void>() { |
| |
| @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; |
| } |
| } |