From 367267b01bcc1ec5965cfc7c26149ccd405c11cf Mon Sep 17 00:00:00 2001 From: Kevin Jin Date: Thu, 5 Mar 2015 15:40:13 -0800 Subject: Add InstrumentationUtils Move runOnMainSyncWithTimeout to InstrumentationUtils Add By.resourceId(int) since InstrumentationUtils should always be available Change-Id: I8e4683030583b88c3e2b777d0b80b5a2f310e279 --- .../android_support_test/D2AndroidJUnitRunner.java | 68 +------- .../appium/droiddriver/manualtest/ManualTest.java | 9 + .../droiddriver/base/DroidDriverContext.java | 83 --------- src/io/appium/droiddriver/finders/By.java | 14 +- .../droiddriver/helpers/BaseDroidDriverTest.java | 26 +-- .../appium/droiddriver/helpers/DroidDrivers.java | 88 +++------- .../helpers/DroidDriversInitializer.java | 49 ++++++ .../instrumentation/InstrumentationDriver.java | 40 ++--- .../instrumentation/InstrumentationUiDevice.java | 62 +++---- .../droiddriver/instrumentation/ViewElement.java | 82 +++++---- src/io/appium/droiddriver/runner/TestRunner.java | 59 +++---- .../droiddriver/util/InstrumentationUtils.java | 189 +++++++++++++++++++++ 12 files changed, 396 insertions(+), 373 deletions(-) create mode 100644 src/io/appium/droiddriver/helpers/DroidDriversInitializer.java create mode 100644 src/io/appium/droiddriver/util/InstrumentationUtils.java diff --git a/droiddriver-android_support_test/src/io/appium/droiddriver/android_support_test/D2AndroidJUnitRunner.java b/droiddriver-android_support_test/src/io/appium/droiddriver/android_support_test/D2AndroidJUnitRunner.java index 7924380..4d916bd 100644 --- a/droiddriver-android_support_test/src/io/appium/droiddriver/android_support_test/D2AndroidJUnitRunner.java +++ b/droiddriver-android_support_test/src/io/appium/droiddriver/android_support_test/D2AndroidJUnitRunner.java @@ -26,13 +26,9 @@ import android.util.Log; import java.util.Iterator; import java.util.concurrent.Callable; -import java.util.concurrent.FutureTask; -import java.util.concurrent.TimeUnit; -import io.appium.droiddriver.exceptions.DroidDriverException; -import io.appium.droiddriver.exceptions.TimeoutException; -import io.appium.droiddriver.helpers.DroidDrivers; import io.appium.droiddriver.util.ActivityUtils; +import io.appium.droiddriver.util.InstrumentationUtils; import io.appium.droiddriver.util.Logs; /** @@ -50,12 +46,11 @@ public class D2AndroidJUnitRunner extends AndroidJUnitRunner { }; /** - * {@inheritDoc}

Sets the values for the convenience methods {@link - * DroidDrivers#getInstrumentation()} and {@link DroidDrivers#getOptions()}. + * {@inheritDoc}

Initializes {@link InstrumentationUtils}. */ @Override public void onCreate(Bundle arguments) { - DroidDrivers.initInstrumentation(this, arguments); + InstrumentationUtils.init(this, arguments); super.onCreate(arguments); } @@ -74,67 +69,14 @@ public class D2AndroidJUnitRunner extends AndroidJUnitRunner { return GET_RUNNING_ACTIVITY.call(); } - return runOnMainSyncWithTimeout(GET_RUNNING_ACTIVITY); + return InstrumentationUtils.runOnMainSyncWithTimeout(GET_RUNNING_ACTIVITY); } catch (Exception e) { Logs.log(Log.WARN, e); return null; } } }); - super.onStart(); - } - - /** - * Runs {@code callable} on the main thread on best-effort basis up to a time limit, which - * defaults to {@code 10000L} and can be set as an - * am instrument option under the key {@code dd.runOnMainSyncTimeout}.

This is a safer - * variation of {@link #runOnMainSync} because the latter may hang. But it is heavy because a new - * thread is created for each call unless the am command line specifies {@code - * dd.runOnMainSyncTimeout <=0} such as "-e dd.runOnMainSyncTimeout 0".

The {@code callable} - * may never run, for example, in case that the main Looper has exited due to uncaught exception. - */ - // TODO: move this to DroidDrivers - // TODO: call runOnMainSync on a single worker thread? - private V runOnMainSyncWithTimeout(Callable callable) { - final RunOnMainSyncFutureTask futureTask = new RunOnMainSyncFutureTask<>(callable); - - String timeoutMillisString = DroidDrivers.getOptions().getString("dd.runOnMainSyncTimeout"); - long timeoutMillis = timeoutMillisString == null? 10000L : Long.parseLong(timeoutMillisString); - if (timeoutMillis <= 0L) { - // Call runOnMainSync on current thread without time limit. - futureTask.runOnMainSyncNoThrow(); - } else { - new Thread() { - @Override - public void run() { - futureTask.runOnMainSyncNoThrow(); - } - }.start(); - } - try { - return futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS); - } catch (java.util.concurrent.TimeoutException e) { - throw new TimeoutException("Timed out after " + timeoutMillis - + " milliseconds waiting for Instrumentation.runOnMainSync", e); - } catch (Throwable e) { - throw new DroidDriverException(e); - } finally { - futureTask.cancel(false); - } - } - - private class RunOnMainSyncFutureTask extends FutureTask { - public RunOnMainSyncFutureTask(Callable callable) { - super(callable); - } - - public void runOnMainSyncNoThrow() { - try { - runOnMainSync(this); - } catch (Throwable e) { - setException(e); - } - } + super.onStart(); } } diff --git a/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java b/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java index 6e95272..83966f7 100644 --- a/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java +++ b/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java @@ -5,6 +5,9 @@ import android.app.Activity; import io.appium.droiddriver.finders.By; import io.appium.droiddriver.finders.Finder; import io.appium.droiddriver.helpers.BaseDroidDriverTest; +import io.appium.droiddriver.helpers.DroidDrivers; +import io.appium.droiddriver.helpers.DroidDriversInitializer; +import io.appium.droiddriver.uiautomation.UiAutomationDriver; /** * This is for manually testing DroidDriver. It is not meant for continuous @@ -24,6 +27,12 @@ public class ManualTest extends BaseDroidDriverTest { super(Activity.class); } + // This does not instrument a certain AUT, so InstrumentationDriver won't work + protected void classSetUp() { + DroidDrivers.checkUiAutomation(); + DroidDriversInitializer.get(new UiAutomationDriver(getInstrumentation())).singleRun(); + } + public void testSetTextForPassword() { Finder password_edit = By.resourceId("com.google.android.gsf.login:id/password_edit"); String oldPassword = "A fake password that is not empty and needs to be cleared by setText"; diff --git a/src/io/appium/droiddriver/base/DroidDriverContext.java b/src/io/appium/droiddriver/base/DroidDriverContext.java index 89e2022..064d472 100644 --- a/src/io/appium/droiddriver/base/DroidDriverContext.java +++ b/src/io/appium/droiddriver/base/DroidDriverContext.java @@ -17,20 +17,11 @@ package io.appium.droiddriver.base; import android.app.Instrumentation; -import android.os.Looper; -import android.util.Log; -import java.util.Locale; import java.util.Map; import java.util.WeakHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; -import java.util.concurrent.TimeUnit; -import io.appium.droiddriver.exceptions.DroidDriverException; -import io.appium.droiddriver.exceptions.TimeoutException; import io.appium.droiddriver.finders.ByXPath; -import io.appium.droiddriver.util.Logs; /** * Internal helper for DroidDriver implementation. @@ -72,78 +63,4 @@ public class DroidDriverContext> { map.clear(); ByXPath.clearData(); } - - /** - * Tries to wait for an idle state on the main thread on best-effort basis up - * to {@code timeoutMillis}. The main thread may not enter the idle state when - * animation is playing, for example, the ProgressBar. - */ - public boolean tryWaitForIdleSync(long timeoutMillis) { - validateNotAppThread(); - FutureTask futureTask = new FutureTask(new Runnable() { - @Override - public void run() {} - }, null); - instrumentation.waitForIdle(futureTask); - - try { - futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - throw new DroidDriverException(e); - } catch (ExecutionException e) { - throw new DroidDriverException(e); - } catch (java.util.concurrent.TimeoutException e) { - Logs.log(Log.DEBUG, String.format(Locale.US, - "Timed out after %d milliseconds waiting for idle on main looper", timeoutMillis)); - return false; - } - return true; - } - - /** - * Tries to run {@code runnable} on the main thread on best-effort basis up to - * {@code timeoutMillis}. The {@code runnable} may never run, for example, in - * case that the main Looper has exited due to uncaught exception. - */ - public boolean tryRunOnMainSync(Runnable runnable, long timeoutMillis) { - validateNotAppThread(); - final FutureTask futureTask = new FutureTask(runnable, null); - new Thread(new Runnable() { - @Override - public void run() { - instrumentation.runOnMainSync(futureTask); - } - }).start(); - - try { - futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - throw new DroidDriverException(e); - } catch (ExecutionException e) { - throw new DroidDriverException(e); - } catch (java.util.concurrent.TimeoutException e) { - Logs.log(Log.WARN, getRunOnMainSyncTimeoutMessage(timeoutMillis)); - return false; - } - return true; - } - - public void runOnMainSync(Runnable runnable) { - long timeoutMillis = getDriver().getPoller().getTimeoutMillis(); - if (!tryRunOnMainSync(runnable, timeoutMillis)) { - throw new TimeoutException(getRunOnMainSyncTimeoutMessage(timeoutMillis)); - } - } - - private String getRunOnMainSyncTimeoutMessage(long timeoutMillis) { - return String.format(Locale.US, - "Timed out after %d milliseconds waiting for Instrumentation.runOnMainSync", timeoutMillis); - } - - private void validateNotAppThread() { - if (Looper.myLooper() == Looper.getMainLooper()) { - throw new DroidDriverException( - "This method can not be called from the main application thread"); - } - } } diff --git a/src/io/appium/droiddriver/finders/By.java b/src/io/appium/droiddriver/finders/By.java index 874cd29..f8ac924 100644 --- a/src/io/appium/droiddriver/finders/By.java +++ b/src/io/appium/droiddriver/finders/By.java @@ -16,8 +16,11 @@ package io.appium.droiddriver.finders; +import android.content.Context; + import io.appium.droiddriver.UiElement; import io.appium.droiddriver.exceptions.ElementNotFoundException; +import io.appium.droiddriver.util.InstrumentationUtils; import static io.appium.droiddriver.util.Preconditions.checkNotNull; @@ -44,7 +47,16 @@ public class By { return new MatchFinder(Predicates.attributeFalse(attribute)); } - /** Matches a UiElement by resource id. */ + /** Matches a UiElement by a resource id defined in the AUT. */ + public static MatchFinder resourceId(int resourceId) { + Context targetContext = InstrumentationUtils.getInstrumentation().getTargetContext(); + return resourceId(targetContext.getResources().getResourceName(resourceId)); + } + + /** + * Matches a UiElement by the string representation of a resource id. This works for resources + * not belonging to the AUT. + */ public static MatchFinder resourceId(String resourceId) { return new MatchFinder(Predicates.attributeEquals(Attribute.RESOURCE_ID, resourceId)); } diff --git a/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java index 7b29bc0..0b17dc5 100644 --- a/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java +++ b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java @@ -17,7 +17,6 @@ package io.appium.droiddriver.helpers; import android.app.Activity; -import android.app.Instrumentation; import android.content.Context; import android.os.Debug; import android.test.FlakyTest; @@ -41,29 +40,6 @@ import io.appium.droiddriver.util.Logs; */ public abstract class BaseDroidDriverTest extends D2ActivityInstrumentationTestCase2 { - /** - * Calls {@link DroidDrivers#init} once and only once. - */ - public static class DroidDriversInitializer extends SingleRun { - private static DroidDriversInitializer instance; - protected final Instrumentation instrumentation; - - protected DroidDriversInitializer(Instrumentation instrumentation) { - this.instrumentation = instrumentation; - } - - @Override - protected void run() { - DroidDrivers.init(DroidDrivers.newDriver(instrumentation)); - } - - public static synchronized DroidDriversInitializer get(Instrumentation instrumentation) { - if (instance == null) { - instance = new DroidDriversInitializer(instrumentation); - } - return instance; - } - } private static boolean classSetUpDone = false; // In case of device-wide fatal errors, e.g. OOME, the remaining tests will @@ -120,7 +96,7 @@ public abstract class BaseDroidDriverTest extends * io.appium.droiddriver.instrumentation.ViewElement#overrideClassName} */ protected void classSetUp() { - DroidDriversInitializer.get(getInstrumentation()).singleRun(); + DroidDriversInitializer.get(DroidDrivers.newDriver()).singleRun(); } protected boolean reportSkippedAsFailed() { diff --git a/src/io/appium/droiddriver/helpers/DroidDrivers.java b/src/io/appium/droiddriver/helpers/DroidDrivers.java index 60c3740..5cddb4f 100644 --- a/src/io/appium/droiddriver/helpers/DroidDrivers.java +++ b/src/io/appium/droiddriver/helpers/DroidDrivers.java @@ -19,7 +19,6 @@ package io.appium.droiddriver.helpers; import android.annotation.TargetApi; import android.app.Instrumentation; import android.os.Build; -import android.os.Bundle; import java.lang.reflect.InvocationTargetException; @@ -27,83 +26,53 @@ import io.appium.droiddriver.DroidDriver; import io.appium.droiddriver.exceptions.DroidDriverException; import io.appium.droiddriver.instrumentation.InstrumentationDriver; import io.appium.droiddriver.uiautomation.UiAutomationDriver; +import io.appium.droiddriver.util.InstrumentationUtils; /** - * Static utility methods pertaining to {@link DroidDriver} instances. + * Static utility methods using a singleton {@link DroidDriver} instance. This class is NOT + * required, but it is handy and using a singleton driver can avoid memory leak when you have many + * instances around (for example, one in every test - JUnit framework keeps the test instances in + * memory after running them). */ public class DroidDrivers { private static DroidDriver driver; - private static Instrumentation instrumentation; - private static Bundle options; /** - * Gets the singleton driver. Throws if {@link #init} has not been called. + * Gets the singleton driver. Throws if {@link #setSingleton} has not been called. */ public static DroidDriver get() { - if (DroidDrivers.driver == null) { - throw new DroidDriverException("init() has not been called"); + if (driver == null) { + throw new DroidDriverException("setSingleton() has not been called"); } - return DroidDrivers.driver; + return driver; } /** - * Initializes the singleton driver. The singleton driver is NOT required, but - * it is handy and using a singleton driver can avoid memory leak if you have - * many instances around (for example, one in every test -- JUnit framework - * keeps the test instances in memory after running them). + * Sets the singleton driver. */ - public static void init(DroidDriver driver) { + public static void setSingleton(DroidDriver driver) { if (DroidDrivers.driver != null) { - throw new DroidDriverException("init() can only be called once"); + throw new DroidDriverException("setSingleton() can only be called once"); } DroidDrivers.driver = driver; } /** - * Initializes for the convenience methods {@link #getInstrumentation()} and - * {@link #getOptions()}. Called by - * {@link io.appium.droiddriver.runner.TestRunner}. If a custom - * runner is used, this method must be called appropriately, otherwise the two - * convenience methods won't work. - */ - public static void initInstrumentation(Instrumentation instrumentation, Bundle arguments) { - if (DroidDrivers.instrumentation != null) { - throw new DroidDriverException("DroidDrivers.initInstrumentation() can only be called once"); - } - DroidDrivers.instrumentation = instrumentation; - DroidDrivers.options = arguments; - } - - public static Instrumentation getInstrumentation() { - return instrumentation; - } - - /** - * Gets the am instrument options. - */ - public static Bundle getOptions() { - return options; - } - - /** - * Returns whether the running target (device or emulator) has - * {@link android.app.UiAutomation} API, which is introduced in SDK API 18 - * (JELLY_BEAN_MR2). + * Returns whether the running target (device or emulator) has {@link android.app.UiAutomation} + * API, which is introduced in SDK API 18 (JELLY_BEAN_MR2). */ public static boolean hasUiAutomation() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; } /** - * Returns a new DroidDriver instance. If am instrument options have "driver", - * treat it as the fully-qualified-class-name and create a new instance of it - * with {@code instrumentation} as the argument; otherwise a new - * platform-dependent default DroidDriver instance. + * Returns a new DroidDriver instance. If am instrument options have "driver", treat it as the + * fully-qualified-class-name and create a new instance of it with {@code instrumentation} as the + * argument; otherwise a new platform-dependent default DroidDriver instance. */ - public static DroidDriver newDriver(Instrumentation instrumentation) { - String driverClass = options == null ? null : options.getString("driver"); + public static DroidDriver newDriver() { + Instrumentation instrumentation = InstrumentationUtils.getInstrumentation(); + String driverClass = InstrumentationUtils.getD2Option("driver"); if (driverClass != null) { try { return (DroidDriver) Class.forName(driverClass).getConstructor(Instrumentation.class) @@ -123,29 +92,24 @@ public class DroidDrivers { } } - // If "driver" is not specified, return default. + // If "dd.driver" is not specified, return default. if (hasUiAutomation()) { - return newUiAutomationDriver(instrumentation); + checkUiAutomation(); + return new UiAutomationDriver(instrumentation); } - return newInstrumentationDriver(instrumentation); - } - - /** Returns a new InstrumentationDriver */ - public static InstrumentationDriver newInstrumentationDriver(Instrumentation instrumentation) { return new InstrumentationDriver(instrumentation); } - /** Returns a new UiAutomationDriver */ + /** Checks if UiAutomation API is available */ @TargetApi(18) - public static UiAutomationDriver newUiAutomationDriver(Instrumentation instrumentation) { + public static void checkUiAutomation() { if (!hasUiAutomation()) { throw new DroidDriverException("UiAutomation is not available below API 18. " + "See http://developer.android.com/reference/android/app/UiAutomation.html"); } - if (instrumentation.getUiAutomation() == null) { + if (InstrumentationUtils.getInstrumentation().getUiAutomation() == null) { throw new DroidDriverException( "uiAutomation==null: did you forget to set '-w' flag for 'am instrument'?"); } - return new UiAutomationDriver(instrumentation); } } diff --git a/src/io/appium/droiddriver/helpers/DroidDriversInitializer.java b/src/io/appium/droiddriver/helpers/DroidDriversInitializer.java new file mode 100644 index 0000000..727c97b --- /dev/null +++ b/src/io/appium/droiddriver/helpers/DroidDriversInitializer.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 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.helpers; + +import io.appium.droiddriver.DroidDriver; +import io.appium.droiddriver.exceptions.UnrecoverableException; + +/** + * Calls {@link DroidDrivers#setSingleton} once and only once. + */ +public class DroidDriversInitializer extends SingleRun { + private static DroidDriversInitializer instance; + private final DroidDriver driver; + + private DroidDriversInitializer(DroidDriver driver) { + this.driver = driver; + } + + @Override + protected void run() { + DroidDrivers.setSingleton(driver); + } + + public static synchronized DroidDriversInitializer get(DroidDriver driver) { + if (instance == null) { + instance = new DroidDriversInitializer(driver); + } + + if (instance.driver != driver) { + throw new UnrecoverableException("The singleton DroidDriversInitializer has already been" + + " created with a different driver"); + } + return instance; + } +} diff --git a/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java b/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java index fa3fb8e..d3e5dd2 100644 --- a/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java +++ b/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java @@ -22,13 +22,14 @@ import android.util.Log; import android.view.View; import java.util.List; +import java.util.concurrent.Callable; 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.InstrumentationUtils; import io.appium.droiddriver.util.Logs; /** @@ -60,39 +61,26 @@ public class InstrumentationDriver extends BaseDroidDriver { return new ViewElement(context, rawElement, parent); } - private static class FindRootViewRunnable implements Runnable { - View rootView; - Throwable exception; - + private static final Callable FIND_ROOT_VIEW = new Callable() { @Override - public void run() { - try { - List 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; - } + public View call() { + List views = RootFinder.getRootViews(); + if (views.size() > 1) { + Logs.log(Log.VERBOSE, "views.size()=" + views.size()); + for (View view : views) { + if (view.hasWindowFocus()) { + return view; } } - // Fall back to DecorView. - rootView = ActivityUtils.getRunningActivity().getWindow().getDecorView(); - } catch (Throwable e) { - exception = e; } + // Fall back to DecorView. + return ActivityUtils.getRunningActivity().getWindow().getDecorView(); } - } + }; private View findRootView() { waitForRunningActivity(); - FindRootViewRunnable findRootViewRunnable = new FindRootViewRunnable(); - context.runOnMainSync(findRootViewRunnable); - if (findRootViewRunnable.exception != null) { - throw new DroidDriverException(findRootViewRunnable.exception); - } - return findRootViewRunnable.rootView; + return InstrumentationUtils.runOnMainSyncWithTimeout(FIND_ROOT_VIEW); } private void waitForRunningActivity() { diff --git a/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java b/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java index 3e3b35c..f485f89 100644 --- a/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java +++ b/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java @@ -24,8 +24,11 @@ import android.graphics.RectF; import android.util.Log; import android.view.View; +import java.util.concurrent.Callable; + import io.appium.droiddriver.base.BaseUiDevice; import io.appium.droiddriver.base.DroidDriverContext; +import io.appium.droiddriver.util.InstrumentationUtils; import io.appium.droiddriver.util.Logs; class InstrumentationUiDevice extends BaseUiDevice { @@ -37,10 +40,13 @@ class InstrumentationUiDevice extends BaseUiDevice { @Override protected Bitmap takeScreenshot() { - ScreenshotRunnable screenshotRunnable = - new ScreenshotRunnable(context.getDriver().getRootElement().getRawElement()); - context.runOnMainSync(screenshotRunnable); - return screenshotRunnable.screenshot; + try { + return InstrumentationUtils.runOnMainSyncWithTimeout(new GetScreenshot( + context.getDriver().getRootElement().getRawElement())); + } catch (Throwable e) { + Logs.log(Log.ERROR, e); + return null; + } } @Override @@ -48,38 +54,36 @@ class InstrumentationUiDevice extends BaseUiDevice { return context; } - private static class ScreenshotRunnable implements Runnable { + private static class GetScreenshot implements Callable { private final View rootView; - Bitmap screenshot; - private ScreenshotRunnable(View rootView) { + private GetScreenshot(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); + public Bitmap call() { + Bitmap screenshot; + 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(); + return screenshot; } } } diff --git a/src/io/appium/droiddriver/instrumentation/ViewElement.java b/src/io/appium/droiddriver/instrumentation/ViewElement.java index a92dee4..e706362 100644 --- a/src/io/appium/droiddriver/instrumentation/ViewElement.java +++ b/src/io/appium/droiddriver/instrumentation/ViewElement.java @@ -30,13 +30,14 @@ import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Callable; 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.InstrumentationUtils; import io.appium.droiddriver.util.Preconditions; import static io.appium.droiddriver.util.Strings.charSequenceToString; @@ -45,55 +46,51 @@ import static io.appium.droiddriver.util.Strings.charSequenceToString; * A UiElement that is backed by a View. */ public class ViewElement extends BaseUiElement { - private static class SnapshotViewAttributesRunnable implements Runnable { + private static class AttributesSnapshot implements Callable { private final View view; final Map attribs = new EnumMap(Attribute.class); boolean visible; Rect visibleBounds; List childViews; - Throwable exception; - private SnapshotViewAttributesRunnable(View view) { + private AttributesSnapshot(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()); - } + public Void call() { + 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; } + 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(); + return null; } private void put(Attribute key, Object value) { @@ -223,11 +220,8 @@ public class ViewElement extends BaseUiElement { 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); - } + AttributesSnapshot attributesSnapshot = new AttributesSnapshot(view); + InstrumentationUtils.runOnMainSyncWithTimeout(attributesSnapshot); attributes = Collections.unmodifiableMap(attributesSnapshot.attribs); this.visibleBounds = attributesSnapshot.visibleBounds; @@ -276,7 +270,7 @@ public class ViewElement extends BaseUiElement { @Override protected void doPerformAndWait(FutureTask futureTask, long timeoutMillis) { futureTask.run(); - context.tryWaitForIdleSync(timeoutMillis); + InstrumentationUtils.tryWaitForIdleSync(timeoutMillis); } @Override diff --git a/src/io/appium/droiddriver/runner/TestRunner.java b/src/io/appium/droiddriver/runner/TestRunner.java index be92b44..71bb744 100644 --- a/src/io/appium/droiddriver/runner/TestRunner.java +++ b/src/io/appium/droiddriver/runner/TestRunner.java @@ -35,12 +35,11 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.FutureTask; -import java.util.concurrent.TimeUnit; import io.appium.droiddriver.helpers.DroidDrivers; import io.appium.droiddriver.util.ActivityUtils; import io.appium.droiddriver.util.ActivityUtils.Supplier; +import io.appium.droiddriver.util.InstrumentationUtils; import io.appium.droiddriver.util.Logs; /** @@ -63,12 +62,11 @@ public class TestRunner extends InstrumentationTestRunner { /** * {@inheritDoc} *

- * Sets the values for the convenience methods {@link DroidDrivers#getInstrumentation()} and - * {@link DroidDrivers#getOptions()}. + * Initializes {@link InstrumentationUtils}. */ @Override public void onCreate(Bundle arguments) { - DroidDrivers.initInstrumentation(this, arguments); + InstrumentationUtils.init(this, arguments); super.onCreate(arguments); } @@ -92,21 +90,25 @@ public class TestRunner extends InstrumentationTestRunner { activitiesCopy = activities.toArray(new Activity[activities.size()]); } - runOnMainSyncWithTimeLimit(new Runnable() { - @Override - public void run() { - for (Activity activity : activitiesCopy) { - if (!activity.isFinishing()) { - try { - Logs.log(Log.INFO, "Stopping activity: " + activity); - activity.finish(); - } catch (Throwable e) { - Logs.log(Log.ERROR, e, "Failed to stop activity"); + try { + InstrumentationUtils.runOnMainSyncWithTimeout(new Runnable() { + @Override + public void run() { + for (Activity activity : activitiesCopy) { + if (!activity.isFinishing()) { + try { + Logs.log(Log.INFO, "Stopping activity: " + activity); + activity.finish(); + } catch (Throwable e) { + Logs.log(Log.ERROR, e, "Failed to stop activity"); + } } } } - } - }); + }); + } catch (Throwable e) { + Logs.log(Log.ERROR, e); + } // We've done what we can. Clear activities if any are left. synchronized (activities) { @@ -198,27 +200,4 @@ public class TestRunner extends InstrumentationTestRunner { runningActivity = null; } } - - private boolean runOnMainSyncWithTimeLimit(Runnable runnable) { - // Do we need it configurable? Now only used in endTest. - long timeoutMillis = 10000L; - final FutureTask futureTask = new FutureTask(runnable, null); - new Thread(new Runnable() { - @Override - public void run() { - runOnMainSync(futureTask); - } - }).start(); - - try { - futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS); - return true; - } catch (Throwable e) { - Logs.log(Log.WARN, e, String.format( - "Timed out after %d milliseconds waiting for Instrumentation.runOnMainSync", - timeoutMillis)); - futureTask.cancel(false); - return false; - } - } } diff --git a/src/io/appium/droiddriver/util/InstrumentationUtils.java b/src/io/appium/droiddriver/util/InstrumentationUtils.java new file mode 100644 index 0000000..a019008 --- /dev/null +++ b/src/io/appium/droiddriver/util/InstrumentationUtils.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2015 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.util; + +import android.app.Instrumentation; +import android.os.Bundle; +import android.os.Looper; +import android.util.Log; + +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; + +import io.appium.droiddriver.exceptions.DroidDriverException; +import io.appium.droiddriver.exceptions.TimeoutException; +import io.appium.droiddriver.exceptions.UnrecoverableException; + +/** + * Static utility methods pertaining to {@link Instrumentation}. + */ +public class InstrumentationUtils { + private static Instrumentation instrumentation; + private static Bundle options; + private static long runOnMainSyncTimeoutMillis; + private static final Runnable EMPTY_RUNNABLE = new Runnable() { + @Override + public void run() { + } + }; + + /** + * Initializes this class. If you use a runner that is not DroidDriver-aware, you need to call + * this method appropriately. See {@link io.appium.droiddriver.runner.TestRunner#onCreate} for + * example. + */ + public static void init(Instrumentation instrumentation, Bundle arguments) { + if (InstrumentationUtils.instrumentation != null) { + throw new DroidDriverException("init() can only be called once"); + } + InstrumentationUtils.instrumentation = instrumentation; + options = arguments; + + String timeoutString = getD2Option("runOnMainSyncTimeout"); + runOnMainSyncTimeoutMillis = timeoutString == null ? 10000L : Long.parseLong(timeoutString); + } + + private static void checkInitialized() { + if (instrumentation == null) { + throw new UnrecoverableException("If you use a runner that is not DroidDriver-aware, you" + + " need to call InstrumentationUtils.init appropriately"); + } + } + + public static Instrumentation getInstrumentation() { + checkInitialized(); + return instrumentation; + } + + /** + * Gets the am instrument options. + */ + public static Bundle getOptions() { + checkInitialized(); + return options; + } + + /** + * Gets the string value associated with the given key. This is preferred over using {@link + * #getOptions} because the returned {@link Bundle} contains only string values - am instrument + * options do not support value types other than string. + */ + public static String getOption(String key) { + return getOptions().getString(key); + } + + /** + * Calls {@link #getOption} with "dd." prefixed to {@code key}. This is for DroidDriver + * implementation to use a consistent pattern for its options. + */ + public static String getD2Option(String key) { + return getOption("dd." + key); + } + + /** + * Tries to wait for an idle state on the main thread on best-effort basis up to {@code + * timeoutMillis}. The main thread may not enter the idle state when animation is playing, for + * example, the ProgressBar. + */ + public static boolean tryWaitForIdleSync(long timeoutMillis) { + validateNotAppThread(); + FutureTask emptyTask = new FutureTask(EMPTY_RUNNABLE, null); + instrumentation.waitForIdle(emptyTask); + + try { + emptyTask.get(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (java.util.concurrent.TimeoutException e) { + Logs.log(Log.INFO, + "Timed out after " + timeoutMillis + " milliseconds waiting for idle on main looper"); + return false; + } catch (Throwable e) { + throw new DroidDriverException(e); + } + return true; + } + + public static void runOnMainSyncWithTimeout(final Runnable runnable) { + runOnMainSyncWithTimeout(new Callable() { + @Override + public Void call() throws Exception { + runnable.run(); + return null; + } + }); + } + + /** + * Runs {@code callable} on the main thread on best-effort basis up to a time limit, which + * defaults to {@code 10000L} and can be set as an am instrument option under the key {@code + * dd.runOnMainSyncTimeout}.

This is a safer variation of {@link Instrumentation#runOnMainSync} + * because the latter may hang. But it is heavy because a new thread is created for each call. You + * may turn off this behavior by setting {@code "-e dd.runOnMainSyncTimeout 0"} on the am command + * line.

The {@code callable} may never run, for example, in case that the main Looper has + * exited due to uncaught exception. + */ + // TODO: call runOnMainSync on a single worker thread? + public static V runOnMainSyncWithTimeout(Callable callable) { + validateNotAppThread(); + final RunOnMainSyncFutureTask futureTask = new RunOnMainSyncFutureTask<>(callable); + + if (runOnMainSyncTimeoutMillis <= 0L) { + // Call runOnMainSync on current thread without time limit. + futureTask.runOnMainSyncNoThrow(); + } else { + new Thread() { + @Override + public void run() { + futureTask.runOnMainSyncNoThrow(); + } + }.start(); + } + + try { + return futureTask.get(runOnMainSyncTimeoutMillis, TimeUnit.MILLISECONDS); + } catch (java.util.concurrent.TimeoutException e) { + throw new TimeoutException("Timed out after " + runOnMainSyncTimeoutMillis + + " milliseconds waiting for Instrumentation.runOnMainSync", e); + } catch (Throwable e) { + throw new DroidDriverException(e); + } finally { + futureTask.cancel(false); + } + } + + private static class RunOnMainSyncFutureTask extends FutureTask { + public RunOnMainSyncFutureTask(Callable callable) { + super(callable); + } + + public void runOnMainSyncNoThrow() { + try { + getInstrumentation().runOnMainSync(this); + } catch (Throwable e) { + setException(e); + } + } + } + + private static void validateNotAppThread() { + if (Looper.myLooper() == Looper.getMainLooper()) { + throw new DroidDriverException( + "This method can not be called from the main application thread"); + } + } +} -- cgit v1.2.3