/* * 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.instrumentation; import static com.google.android.droiddriver.util.TextUtils.charSequenceToString; import android.content.res.Resources; import android.graphics.Rect; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Checkable; import android.widget.TextView; import com.google.android.droiddriver.InputInjector; import com.google.android.droiddriver.base.AbstractUiElement; import com.google.android.droiddriver.util.Logs; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; import java.util.Map; /** * A UiElement that is backed by a View. */ // TODO: always accessing view on the UI thread even when only get access is // needed -- the field may be in the middle of updating. public class ViewElement extends AbstractUiElement { private static final Map CLASS_NAME_OVERRIDES = Maps.newHashMap(); private final InstrumentationContext context; private final View view; /** * 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}). *

* If tests fail with InstrumentationDriver, find the actual class name by * examining app code or by calling * {@link com.google.android.droiddriver.DroidDriver#dumpUiElementTree}, then * call this method in setUp to override it with the class name seen in * uiautomatorviewer. */ public static void overrideClassName(String actualClassName, String overridingClassName) { CLASS_NAME_OVERRIDES.put(actualClassName, overridingClassName); } public ViewElement(InstrumentationContext context, View view) { this.context = Preconditions.checkNotNull(context); this.view = Preconditions.checkNotNull(view); } @Override public String getText() { if (!(view instanceof TextView)) { return null; } return charSequenceToString(((TextView) view).getText()); } @Override public String getContentDescription() { return charSequenceToString(view.getContentDescription()); } @Override public String getClassName() { String className = view.getClass().getName(); return CLASS_NAME_OVERRIDES.containsKey(className) ? CLASS_NAME_OVERRIDES.get(className) : className; } @Override public 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; } @Override public String getPackageName() { return view.getContext().getPackageName(); } @Override public InputInjector getInjector() { return context.getInjector(); } @Override public boolean isVisible() { // 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. return view.isShown() && !getVisibleBounds().isEmpty(); } @Override public boolean isCheckable() { return view instanceof Checkable; } @Override public boolean isChecked() { if (!isCheckable()) { return false; } return ((Checkable) view).isChecked(); } @Override public boolean isClickable() { return view.isClickable(); } @Override public boolean isEnabled() { return view.isEnabled(); } @Override public boolean isFocusable() { return view.isFocusable(); } @Override public boolean isFocused() { return view.isFocused(); } @Override public boolean isScrollable() { // TODO: find a meaningful implementation return true; } @Override public boolean isLongClickable() { return view.isLongClickable(); } @Override public boolean isPassword() { // TODO: find a meaningful implementation return false; } @Override public boolean isSelected() { return view.isSelected(); } @Override public 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; } @Override public Rect getVisibleBounds() { Rect visibleBounds = new Rect(); if (!view.getGlobalVisibleRect(visibleBounds)) { Logs.log(Log.VERBOSE, "View is invisible: " + toString()); visibleBounds.setEmpty(); } int[] xy = new int[2]; view.getLocationOnScreen(xy); // Bounds are relative to root view; adjust to screen coordinates. visibleBounds.offsetTo(xy[0], xy[1]); return visibleBounds; } @Override public int getChildCount() { if (!(view instanceof ViewGroup)) { return 0; } return ((ViewGroup) view).getChildCount(); } @Override public ViewElement getChild(int index) { if (!(view instanceof ViewGroup)) { return null; } View child = ((ViewGroup) view).getChildAt(index); return child == null ? null : context.getUiElement(child); } @Override public ViewElement getParent() { ViewParent parent = view.getParent(); if (!(parent instanceof View)) { return null; } return context.getUiElement((View) parent); } }