diff options
author | Kevin Jin <kjin@google.com> | 2015-02-20 09:35:39 -0800 |
---|---|---|
committer | Kevin Jin <kjin@google.com> | 2015-02-20 14:37:53 -0800 |
commit | 4b31201b5a2dbf8036da5a8d089a68a39cc1dc44 (patch) | |
tree | 0a4a6d976ca45f3b87433927d57d50cb3cd51b41 /src/io/appium/droiddriver/instrumentation | |
parent | 85a1731f32032690e528a6ca1084aa148200569b (diff) | |
download | droiddriver-4b31201b5a2dbf8036da5a8d089a68a39cc1dc44.tar.gz |
rename package 'com.google.android' to 'io.appium'
Change-Id: I2c7c96cd6a6971806e2ea7b06cd6c2c6666e4340
Diffstat (limited to 'src/io/appium/droiddriver/instrumentation')
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; + } +} |