aboutsummaryrefslogtreecommitdiff
path: root/src/io/appium/droiddriver/instrumentation
diff options
context:
space:
mode:
authorKevin Jin <kjin@google.com>2015-02-20 09:35:39 -0800
committerKevin Jin <kjin@google.com>2015-02-20 14:37:53 -0800
commit4b31201b5a2dbf8036da5a8d089a68a39cc1dc44 (patch)
tree0a4a6d976ca45f3b87433927d57d50cb3cd51b41 /src/io/appium/droiddriver/instrumentation
parent85a1731f32032690e528a6ca1084aa148200569b (diff)
downloaddroiddriver-4b31201b5a2dbf8036da5a8d089a68a39cc1dc44.tar.gz
rename package 'com.google.android' to 'io.appium'
Change-Id: I2c7c96cd6a6971806e2ea7b06cd6c2c6666e4340
Diffstat (limited to 'src/io/appium/droiddriver/instrumentation')
-rw-r--r--src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java118
-rw-r--r--src/io/appium/droiddriver/instrumentation/InstrumentationInputInjector.java45
-rw-r--r--src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java85
-rw-r--r--src/io/appium/droiddriver/instrumentation/RootFinder.java95
-rw-r--r--src/io/appium/droiddriver/instrumentation/ViewElement.java286
5 files changed, 629 insertions, 0 deletions
diff --git a/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java b/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java
new file mode 100644
index 0000000..fa3fb8e
--- /dev/null
+++ b/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java
@@ -0,0 +1,118 @@
+/*
+ * 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.app.Instrumentation;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.View;
+
+import java.util.List;
+
+import io.appium.droiddriver.actions.InputInjector;
+import io.appium.droiddriver.base.BaseDroidDriver;
+import io.appium.droiddriver.base.DroidDriverContext;
+import io.appium.droiddriver.exceptions.DroidDriverException;
+import io.appium.droiddriver.exceptions.NoRunningActivityException;
+import io.appium.droiddriver.util.ActivityUtils;
+import io.appium.droiddriver.util.Logs;
+
+/**
+ * Implementation of DroidDriver that is driven via instrumentation.
+ */
+public class InstrumentationDriver extends BaseDroidDriver<View, ViewElement> {
+ private final DroidDriverContext<View, ViewElement> context;
+ private final InputInjector injector;
+ private final InstrumentationUiDevice uiDevice;
+
+ public InstrumentationDriver(Instrumentation instrumentation) {
+ context = new DroidDriverContext<View, ViewElement>(instrumentation, this);
+ injector = new InstrumentationInputInjector(instrumentation);
+ uiDevice = new InstrumentationUiDevice(context);
+ }
+
+ @Override
+ public InputInjector getInjector() {
+ return injector;
+ }
+
+ @Override
+ protected ViewElement newRootElement() {
+ return context.newRootElement(findRootView());
+ }
+
+ @Override
+ protected ViewElement newUiElement(View rawElement, ViewElement parent) {
+ return new ViewElement(context, rawElement, parent);
+ }
+
+ private static class FindRootViewRunnable implements Runnable {
+ View rootView;
+ Throwable exception;
+
+ @Override
+ public void run() {
+ try {
+ List<View> views = RootFinder.getRootViews();
+ if (views.size() > 1) {
+ Logs.log(Log.VERBOSE, "views.size()=" + views.size());
+ for (View view : views) {
+ if (view.hasWindowFocus()) {
+ rootView = view;
+ return;
+ }
+ }
+ }
+ // Fall back to DecorView.
+ rootView = ActivityUtils.getRunningActivity().getWindow().getDecorView();
+ } catch (Throwable e) {
+ exception = e;
+ }
+ }
+ }
+
+ private View findRootView() {
+ waitForRunningActivity();
+ FindRootViewRunnable findRootViewRunnable = new FindRootViewRunnable();
+ context.runOnMainSync(findRootViewRunnable);
+ if (findRootViewRunnable.exception != null) {
+ throw new DroidDriverException(findRootViewRunnable.exception);
+ }
+ return findRootViewRunnable.rootView;
+ }
+
+ private void waitForRunningActivity() {
+ long timeoutMillis = getPoller().getTimeoutMillis();
+ long end = SystemClock.uptimeMillis() + timeoutMillis;
+ while (true) {
+ if (ActivityUtils.getRunningActivity() != null) {
+ return;
+ }
+ long remainingMillis = end - SystemClock.uptimeMillis();
+ if (remainingMillis < 0) {
+ throw new NoRunningActivityException(String.format(
+ "Cannot find the running activity after %d milliseconds", timeoutMillis));
+ }
+ SystemClock.sleep(Math.min(250, remainingMillis));
+ }
+ }
+
+ @Override
+ public InstrumentationUiDevice getUiDevice() {
+ return uiDevice;
+ }
+}
diff --git a/src/io/appium/droiddriver/instrumentation/InstrumentationInputInjector.java b/src/io/appium/droiddriver/instrumentation/InstrumentationInputInjector.java
new file mode 100644
index 0000000..52f4730
--- /dev/null
+++ b/src/io/appium/droiddriver/instrumentation/InstrumentationInputInjector.java
@@ -0,0 +1,45 @@
+/*
+ * 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.app.Instrumentation;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import io.appium.droiddriver.actions.InputInjector;
+import io.appium.droiddriver.exceptions.ActionException;
+
+public class InstrumentationInputInjector implements InputInjector {
+ private final Instrumentation instrumentation;
+
+ public InstrumentationInputInjector(Instrumentation instrumentation) {
+ this.instrumentation = instrumentation;
+ }
+
+ @Override
+ public boolean injectInputEvent(InputEvent event) {
+ if (event instanceof MotionEvent) {
+ instrumentation.sendPointerSync((MotionEvent) event);
+ } else if (event instanceof KeyEvent) {
+ instrumentation.sendKeySync((KeyEvent) event);
+ } else {
+ throw new ActionException("Unknown input event type: " + event);
+ }
+ return true;
+ }
+}
diff --git a/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java b/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java
new file mode 100644
index 0000000..3e3b35c
--- /dev/null
+++ b/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java
@@ -0,0 +1,85 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.Log;
+import android.view.View;
+
+import io.appium.droiddriver.base.BaseUiDevice;
+import io.appium.droiddriver.base.DroidDriverContext;
+import io.appium.droiddriver.util.Logs;
+
+class InstrumentationUiDevice extends BaseUiDevice {
+ private final DroidDriverContext<View, ViewElement> context;
+
+ InstrumentationUiDevice(DroidDriverContext<View, ViewElement> context) {
+ this.context = context;
+ }
+
+ @Override
+ protected Bitmap takeScreenshot() {
+ ScreenshotRunnable screenshotRunnable =
+ new ScreenshotRunnable(context.getDriver().getRootElement().getRawElement());
+ context.runOnMainSync(screenshotRunnable);
+ return screenshotRunnable.screenshot;
+ }
+
+ @Override
+ protected DroidDriverContext<View, ViewElement> getContext() {
+ return context;
+ }
+
+ private static class ScreenshotRunnable implements Runnable {
+ private final View rootView;
+ Bitmap screenshot;
+
+ private ScreenshotRunnable(View rootView) {
+ this.rootView = rootView;
+ }
+
+ @Override
+ public void run() {
+ try {
+ rootView.destroyDrawingCache();
+ rootView.buildDrawingCache(false);
+ Bitmap drawingCache = rootView.getDrawingCache();
+ int[] xy = new int[2];
+ rootView.getLocationOnScreen(xy);
+ if (xy[0] == 0 && xy[1] == 0) {
+ screenshot = Bitmap.createBitmap(drawingCache);
+ } else {
+ Canvas canvas = new Canvas();
+ Rect rect = new Rect(0, 0, drawingCache.getWidth(), drawingCache.getHeight());
+ rect.offset(xy[0], xy[1]);
+ screenshot =
+ Bitmap.createBitmap(rect.width() + xy[0], rect.height() + xy[1], Config.ARGB_8888);
+ canvas.setBitmap(screenshot);
+ canvas.drawBitmap(drawingCache, null, new RectF(rect), null);
+ canvas.setBitmap(null);
+ }
+ rootView.destroyDrawingCache();
+ } catch (Throwable e) {
+ Logs.log(Log.ERROR, e);
+ }
+ }
+ }
+}
diff --git a/src/io/appium/droiddriver/instrumentation/RootFinder.java b/src/io/appium/droiddriver/instrumentation/RootFinder.java
new file mode 100644
index 0000000..0cdc54e
--- /dev/null
+++ b/src/io/appium/droiddriver/instrumentation/RootFinder.java
@@ -0,0 +1,95 @@
+/*
+ * 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.os.Build;
+import android.view.View;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+
+import io.appium.droiddriver.exceptions.DroidDriverException;
+
+/**
+ * Class to find the root view.
+ */
+public class RootFinder {
+
+ private static final String VIEW_FIELD_NAME = "mViews";
+ private static final Field viewsField;
+ private static final Object windowManagerObj;
+
+ static {
+ String windowManagerClassName =
+ Build.VERSION.SDK_INT >= 17 ? "android.view.WindowManagerGlobal"
+ : "android.view.WindowManagerImpl";
+ String instanceMethod = Build.VERSION.SDK_INT >= 17 ? "getInstance" : "getDefault";
+ try {
+ Class<?> clazz = Class.forName(windowManagerClassName);
+ Method getMethod = clazz.getMethod(instanceMethod);
+ windowManagerObj = getMethod.invoke(null);
+ viewsField = clazz.getDeclaredField(VIEW_FIELD_NAME);
+ viewsField.setAccessible(true);
+ } catch (InvocationTargetException ite) {
+ throw new DroidDriverException(String.format("could not invoke: %s on %s", instanceMethod,
+ windowManagerClassName), ite.getCause());
+ } catch (ClassNotFoundException cnfe) {
+ throw new DroidDriverException(String.format("could not find class: %s",
+ windowManagerClassName), cnfe);
+ } catch (NoSuchFieldException nsfe) {
+ throw new DroidDriverException(String.format("could not find field: %s on %s",
+ VIEW_FIELD_NAME, windowManagerClassName), nsfe);
+ } catch (NoSuchMethodException nsme) {
+ throw new DroidDriverException(String.format("could not find method: %s on %s",
+ instanceMethod, windowManagerClassName), nsme);
+ } catch (RuntimeException re) {
+ throw new DroidDriverException(String.format(
+ "reflective setup failed using obj: %s method: %s field: %s", windowManagerClassName,
+ instanceMethod, VIEW_FIELD_NAME), re);
+ } catch (IllegalAccessException iae) {
+ throw new DroidDriverException(String.format(
+ "reflective setup failed using obj: %s method: %s field: %s", windowManagerClassName,
+ instanceMethod, VIEW_FIELD_NAME), iae);
+ }
+ }
+
+ /**
+ * @return a list of {@link View}s.
+ */
+ @SuppressWarnings("unchecked")
+ public static List<View> getRootViews() {
+ List<View> views = null;
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ views = (List<View>) viewsField.get(windowManagerObj);
+ } else {
+ views = Arrays.asList((View[]) viewsField.get(windowManagerObj));
+ }
+ return views;
+ } catch (RuntimeException re) {
+ throw new DroidDriverException(String.format("Reflective access to %s on %s failed.",
+ viewsField, windowManagerObj), re);
+ } catch (IllegalAccessException iae) {
+ throw new DroidDriverException(String.format("Reflective access to %s on %s failed.",
+ viewsField, windowManagerObj), iae);
+ }
+ }
+}
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;
+ }
+}