diff options
Diffstat (limited to 'espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base')
19 files changed, 0 insertions, 2843 deletions
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitor.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitor.java deleted file mode 100644 index 082c045..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitor.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import java.util.concurrent.BrokenBarrierException; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Provides a way to monitor AsyncTask's work queue to ensure that there is no work pending - * or executing (and to allow notification of idleness). - * - * This class is based on the assumption that we can get at the ThreadPoolExecutor AsyncTask uses. - * That is currently possible and easy in Froyo to JB. If it ever becomes impossible, as long as we - * know the max # of executor threads the AsyncTask framework allows we can still use this - * interface, just need a different implementation. - */ -class AsyncTaskPoolMonitor { - private final AtomicReference<IdleMonitor> monitor = new AtomicReference<IdleMonitor>(null); - private final ThreadPoolExecutor pool; - private final AtomicInteger activeBarrierChecks = new AtomicInteger(0); - - AsyncTaskPoolMonitor(ThreadPoolExecutor pool) { - this.pool = checkNotNull(pool); - } - - /** - * Checks if the pool is idle at this moment. - * - * @return true if the pool is idle, false otherwise. - */ - boolean isIdleNow() { - if (!pool.getQueue().isEmpty()) { - return false; - } else { - int activeCount = pool.getActiveCount(); - if (0 != activeCount) { - if (monitor.get() == null) { - // if there's no idle monitor scheduled and there are still barrier - // checks running, they are about to exit, ignore them. - activeCount = activeCount - activeBarrierChecks.get(); - } - } - return 0 == activeCount; - } - } - - /** - * Notifies caller once the pool is idle. - * - * We check for idle-ness by submitting the max # of tasks the pool will take and blocking - * the tasks until they are all executing. Then we know there are no other tasks _currently_ - * executing in the pool, we look back at the work queue to see if its backed up, if it is - * we reenqueue ourselves and try again. - * - * Obviously this strategy will fail horribly if 2 parties are doing it at the same time, - * we prevent recursion here the best we can. - * - * @param idleCallback called once the pool is idle. - */ - void notifyWhenIdle(final Runnable idleCallback) { - checkNotNull(idleCallback); - IdleMonitor myMonitor = new IdleMonitor(idleCallback); - checkState(monitor.compareAndSet(null, myMonitor), "cannot monitor for idle recursively!"); - myMonitor.monitorForIdle(); - } - - /** - * Stops the idle monitoring mechanism if it is in place. - * - * Note: the callback may still be invoked after this method is called. The only thing - * this method guarantees is that we will stop/cancel any blockign tasks we've placed - * on the thread pool. - */ - void cancelIdleMonitor() { - IdleMonitor myMonitor = monitor.getAndSet(null); - if (null != myMonitor) { - myMonitor.poison(); - } - } - - private class IdleMonitor { - private final Runnable onIdle; - private final AtomicInteger barrierGeneration = new AtomicInteger(0); - private final CyclicBarrier barrier; - // written by main, read by all. - private volatile boolean poisoned; - - private IdleMonitor(final Runnable onIdle) { - this.onIdle = checkNotNull(onIdle); - this.barrier = new CyclicBarrier(pool.getCorePoolSize(), - new Runnable() { - @Override - public void run() { - if (pool.getQueue().isEmpty()) { - // no one is behind us, so the queue is idle! - monitor.compareAndSet(IdleMonitor.this, null); - onIdle.run(); - } else { - // work is waiting behind us, enqueue another block of tasks and - // hopefully when they're all running, the queue will be empty. - monitorForIdle(); - } - - } - }); - } - - /** - * Stops this monitor from using the thread pool's resources, it may still cause the - * callback to be executed though. - */ - private void poison() { - poisoned = true; - barrier.reset(); - } - - private void monitorForIdle() { - if (poisoned) { - return; - } - - if (isIdleNow()) { - monitor.compareAndSet(this, null); - onIdle.run(); - } else { - // Submit N tasks that will block until they are all running on the thread pool. - // at this point we can check the pool's queue and verify that there are no new - // tasks behind us and deem the queue idle. - - int poolSize = pool.getCorePoolSize(); - final BarrierRestarter restarter = new BarrierRestarter(barrier, barrierGeneration); - - for (int i = 0; i < poolSize; i++) { - pool.execute(new Runnable() { - @Override - public void run() { - while (!poisoned) { - activeBarrierChecks.incrementAndGet(); - int myGeneration = barrierGeneration.get(); - try { - barrier.await(); - return; - } catch (InterruptedException ie) { - // sorry - I cant let you interrupt me! - restarter.restart(myGeneration); - } catch (BrokenBarrierException bbe) { - restarter.restart(myGeneration); - } finally { - activeBarrierChecks.decrementAndGet(); - } - } - } - }); - } - } - } - } - - - private static class BarrierRestarter { - private final CyclicBarrier barrier; - private final AtomicInteger barrierGeneration; - BarrierRestarter(CyclicBarrier barrier, AtomicInteger barrierGeneration) { - this.barrier = barrier; - this.barrierGeneration = barrierGeneration; - } - - /** - * restarts the barrier. - * - * After the calling this function it is guaranteed that barrier generation has been incremented - * and the barrier can be awaited on again. - * - * @param fromGeneration the generation that encountered the breaking exception. - */ - synchronized void restart(int fromGeneration) { - // must be synchronized. T1 could pass the if check, be suspended before calling reset, T2 - // sails thru - and awaits on the barrier again before T1 has awoken and reset it. - int nextGen = fromGeneration + 1; - if (barrierGeneration.compareAndSet(fromGeneration, nextGen)) { - // first time we've seen fromGeneration request a reset. lets reset the barrier. - barrier.reset(); - } else { - // some other thread has already reset the barrier - this request is a no op. - } - } - } -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/BaseLayerModule.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/BaseLayerModule.java deleted file mode 100644 index 6615b1d..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/BaseLayerModule.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitor; -import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitorRegistry; -import com.google.android.apps.common.testing.testrunner.InstrumentationRegistry; -import com.google.android.apps.common.testing.testrunner.inject.TargetContext; -import com.google.android.apps.common.testing.ui.espresso.FailureHandler; -import com.google.android.apps.common.testing.ui.espresso.Root; -import com.google.android.apps.common.testing.ui.espresso.UiController; -import com.google.common.base.Optional; - -import android.content.Context; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; - -import dagger.Module; -import dagger.Provides; - -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.atomic.AtomicReference; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Dagger module for creating the implementation classes within the base package. - */ -@Module(library = true, injects = { - BaseLayerModule.FailureHandlerHolder.class, FailureHandler.class}) -public class BaseLayerModule { - - @Provides @Singleton - public ActivityLifecycleMonitor provideLifecycleMonitor() { - // TODO(user): replace with installation of AndroidInstrumentationModule once - // proguard issues resolved. - return ActivityLifecycleMonitorRegistry.getInstance(); - } - - @Provides @TargetContext - public Context provideTargetContext() { - // TODO(user): replace with installation of AndroidInstrumentationModule once - // proguard issues resolved. - return InstrumentationRegistry.getInstance().getTargetContext(); - } - - @Provides @Singleton - public Looper provideMainLooper() { - return Looper.getMainLooper(); - } - - @Provides - public UiController provideUiController(UiControllerImpl uiControllerImpl) { - return uiControllerImpl; - } - - @Provides @Singleton @CompatAsyncTask - public Optional<AsyncTaskPoolMonitor> provideCompatAsyncTaskMonitor( - ThreadPoolExecutorExtractor extractor) { - Optional<ThreadPoolExecutor> compatThreadPool = extractor.getCompatAsyncTaskThreadPool(); - if (compatThreadPool.isPresent()) { - return Optional.of(new AsyncTaskPoolMonitor(compatThreadPool.get())); - } else { - return Optional.<AsyncTaskPoolMonitor>absent(); - } - } - - @Provides @Singleton @MainThread - public Executor provideMainThreadExecutor(Looper mainLooper) { - final Handler handler = new Handler(mainLooper); - return new Executor() { - @Override - public void execute(Runnable runnable) { - handler.post(runnable); - } - }; - } - - @Provides @Singleton @SdkAsyncTask - public AsyncTaskPoolMonitor provideSdkAsyncTaskMonitor(ThreadPoolExecutorExtractor extractor) { - return new AsyncTaskPoolMonitor(extractor.getAsyncTaskThreadPool()); - - } - - @Provides - public List<Root> provideKnownRoots(RootsOracle rootsOracle) { - // RootsOracle acts as a provider, but returning Providers is illegal, so delegate. - return rootsOracle.get(); - } - - @Provides @Singleton - public EventInjector provideEventInjector() { - // On API 16 and above, android uses input manager to inject events. On API < 16, - // they use Window Manager. So we need to create our InjectionStrategy depending on the api - // level. Instrumentation does not check if the event presses went through by checking the - // boolean return value of injectInputEvent, which is why we created this class to better - // handle lost/dropped press events. Instrumentation cannot be used as a fallback strategy, - // since this will be executed on the main thread. - int sdkVersion = Build.VERSION.SDK_INT; - EventInjectionStrategy injectionStrategy = null; - if (sdkVersion >= 16) { // Use InputManager for API level 16 and up. - InputManagerEventInjectionStrategy strategy = new InputManagerEventInjectionStrategy(); - strategy.initialize(); - injectionStrategy = strategy; - } else if (sdkVersion >= 7) { - // else Use WindowManager for API level 15 through 7. - WindowManagerEventInjectionStrategy strategy = new WindowManagerEventInjectionStrategy(); - strategy.initialize(); - injectionStrategy = strategy; - } else { - throw new RuntimeException( - "API Level 6 and below is not supported. You are running: " + sdkVersion); - } - return new EventInjector(injectionStrategy); - } - - /** - * Holder for AtomicReference<FailureHandler> which allows updating it at runtime. - */ - @Singleton - public static class FailureHandlerHolder { - private final AtomicReference<FailureHandler> holder; - - @Inject - public FailureHandlerHolder(@Default FailureHandler defaultHandler) { - holder = new AtomicReference<FailureHandler>(defaultHandler); - } - - public void update(FailureHandler handler) { - holder.set(handler); - } - - public FailureHandler get() { - return holder.get(); - } - } - - @Provides - FailureHandler provideFailureHandler(FailureHandlerHolder holder) { - return holder.get(); - } - - @Provides - @Default - FailureHandler provideFailureHander(DefaultFailureHandler impl) { - return impl; - } - -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/CompatAsyncTask.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/CompatAsyncTask.java deleted file mode 100644 index 11ec6ab..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/CompatAsyncTask.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.inject.Qualifier; - -/** - * Annotates a AsyncTaskMonitor as monitoring the CompatAsyncTask pool - */ -@Qualifier -@Retention(RetentionPolicy.RUNTIME) -@interface CompatAsyncTask { } diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/Default.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/Default.java deleted file mode 100644 index b54440d..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/Default.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.inject.Qualifier; - -/** - * Annotates a default provider. - */ -@Qualifier -@Retention(RetentionPolicy.RUNTIME) -public @interface Default { -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandler.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandler.java deleted file mode 100644 index b1e43da..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandler.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Throwables.propagate; - -import com.google.android.apps.common.testing.testrunner.inject.TargetContext; -import com.google.android.apps.common.testing.ui.espresso.EspressoException; -import com.google.android.apps.common.testing.ui.espresso.FailureHandler; -import com.google.android.apps.common.testing.ui.espresso.PerformException; - -import android.content.Context; -import android.view.View; - -import junit.framework.AssertionFailedError; - -import org.hamcrest.Matcher; - -import java.util.concurrent.atomic.AtomicInteger; - -import javax.inject.Inject; - -/** - * Espresso's default {@link FailureHandler}. If this does not fit your needs, feel free to provide - * your own implementation via Espresso.setFailureHandler(FailureHandler). - */ -public final class DefaultFailureHandler implements FailureHandler { - - private static final AtomicInteger failureCount = new AtomicInteger(0); - private final Context appContext; - - @Inject - public DefaultFailureHandler(@TargetContext Context appContext) { - this.appContext = checkNotNull(appContext); - } - - @Override - public void handle(Throwable error, Matcher<View> viewMatcher) { - if (error instanceof EspressoException || error instanceof AssertionFailedError - || error instanceof AssertionError) { - throw propagate(getUserFriendlyError(error, viewMatcher)); - } else { - throw propagate(error); - } - } - - /** - * When the error is coming from espresso, it is more user friendly to: - * 1. propagate assertions as assertions - * 2. swap the stack trace of the error to that of current thread (which will show - * directly where the actual problem is) - */ - private Throwable getUserFriendlyError(Throwable error, Matcher<View> viewMatcher) { - if (error instanceof PerformException) { - // Re-throw the exception with the viewMatcher (used to locate the view) as the view - // description (makes the error more readable). The reason we do this here: not all creators - // of PerformException have access to the viewMatcher. - throw new PerformException.Builder() - .from((PerformException) error) - .withViewDescription(viewMatcher.toString()) - .build(); - } - - if (error instanceof AssertionError) { - // reports Failure instead of Error. - // assertThat(...) throws an AssertionFailedError. - error = new AssertionFailedWithCauseError(error.getMessage(), error); - } - - error.setStackTrace(Thread.currentThread().getStackTrace()); - return error; - } - - private static final class AssertionFailedWithCauseError extends AssertionFailedError { - /* junit hides the cause constructor. */ - public AssertionFailedWithCauseError(String message, Throwable cause) { - super(message); - initCause(cause); - } - } -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectionStrategy.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectionStrategy.java deleted file mode 100644 index 3378197..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectionStrategy.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException; - -import android.view.KeyEvent; -import android.view.MotionEvent; - -/** - * Injects Events into the application under test. Implementors should expect to be called - * from the UI thread and are responsible for ensuring the event gets delivered or indicating that - * it could not be delivered. - */ -interface EventInjectionStrategy { - /** - * Injects the given {@link KeyEvent} into the android system. - * - * @param keyEvent The event to inject - * @return {@code true} if the input was inject successfully, {@code false} otherwise. - * @throws InjectEventSecurityException if the MotionEvent would be delivered to an area of the - * screen that is not owned by the application under test. - */ - boolean injectKeyEvent(KeyEvent keyEvent) throws InjectEventSecurityException; - - /** - * Injects the given {@link MotionEvent} into the android system. - * - * @param motionEvent The event to inject - * @return {@code true} if the input was inject successfully, {@code false} otherwise. - * @throws InjectEventSecurityException if the MotionEvent would be delivered to an area of the - * screen that is not owned by the application under test. - */ - boolean injectMotionEvent(MotionEvent motionEvent) throws InjectEventSecurityException; - -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjector.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjector.java deleted file mode 100644 index 0728331..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjector.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException; - -import android.os.Build; -import android.os.SystemClock; -import android.util.Log; -import android.view.KeyEvent; -import android.view.MotionEvent; - -/** - * Responsible for selecting the proper strategy for injecting MotionEvents to the application under - * test. - */ -final class EventInjector { - private static final String TAG = EventInjector.class.getSimpleName(); - private final EventInjectionStrategy injectionStrategy; - - EventInjector(EventInjectionStrategy injectionStrategy) { - this.injectionStrategy = checkNotNull(injectionStrategy); - } - - boolean injectKeyEvent(KeyEvent event) throws InjectEventSecurityException { - long downTime = event.getDownTime(); - long eventTime = event.getEventTime(); - int action = event.getAction(); - int code = event.getKeyCode(); - int repeatCount = event.getRepeatCount(); - int metaState = event.getMetaState(); - int deviceId = event.getDeviceId(); - int scancode = event.getScanCode(); - int flags = event.getFlags(); - - if (eventTime == 0) { - eventTime = SystemClock.uptimeMillis(); - } - - if (downTime == 0) { - downTime = eventTime; - } - - // API < 9 does not have constructor with source (nor has source field). - KeyEvent newEvent; - if (Build.VERSION.SDK_INT < 9) { - newEvent = new KeyEvent(downTime, - eventTime, - action, - code, - repeatCount, - metaState, - deviceId, - scancode, - flags | KeyEvent.FLAG_FROM_SYSTEM); - } else { - int source = event.getSource(); - newEvent = new KeyEvent(downTime, - eventTime, - action, - code, - repeatCount, - metaState, - deviceId, - scancode, - flags | KeyEvent.FLAG_FROM_SYSTEM, - source); - } - - Log.v( - "ESP_TRACE", - String.format( - "%s:Injecting event for character (%c) with key code (%s) downtime: (%s)", TAG, - newEvent.getUnicodeChar(), newEvent.getKeyCode(), newEvent.getDownTime())); - - return injectionStrategy.injectKeyEvent(newEvent); - } - - boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityException { - return injectionStrategy.injectMotionEvent(event); - } - -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistry.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistry.java deleted file mode 100644 index e390f0f..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistry.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import com.google.android.apps.common.testing.ui.espresso.IdlingPolicies; -import com.google.android.apps.common.testing.ui.espresso.IdlingPolicy; -import com.google.android.apps.common.testing.ui.espresso.IdlingResource; -import com.google.android.apps.common.testing.ui.espresso.IdlingResource.ResourceCallback; -import com.google.common.collect.Lists; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.util.Log; - -import java.util.BitSet; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Keeps track of user-registered {@link IdlingResource}s. - */ -@Singleton -public final class IdlingResourceRegistry { - private static final String TAG = IdlingResourceRegistry.class.getSimpleName(); - - private static final int DYNAMIC_RESOURCE_HAS_IDLED = 1; - private static final int TIMEOUT_OCCURRED = 2; - private static final int IDLE_WARNING_REACHED = 3; - private static final int POSSIBLE_RACE_CONDITION_DETECTED = 4; - private static final Object TIMEOUT_MESSAGE_TAG = new Object(); - - private static final IdleNotificationCallback NO_OP_CALLBACK = new IdleNotificationCallback() { - - @Override - public void allResourcesIdle() {} - - @Override - public void resourcesStillBusyWarning(List<String> busys) {} - - @Override - public void resourcesHaveTimedOut(List<String> busys) {} - }; - - // resources and idleState should only be accessed on main thread - private final List<IdlingResource> resources = Lists.newArrayList(); - // idleState.get(i) == true indicates resources.get(i) is idle, false indicates it's busy - private final BitSet idleState = new BitSet(); - private final Looper looper; - private final Handler handler; - private final Dispatcher dispatcher; - private IdleNotificationCallback idleNotificationCallback = NO_OP_CALLBACK; - - @Inject - public IdlingResourceRegistry(Looper looper) { - this.looper = looper; - this.dispatcher = new Dispatcher(); - this.handler = new Handler(looper, dispatcher); - } - - /** - * Registers the given resource. - */ - public void register(final IdlingResource resource) { - checkNotNull(resource); - if (Looper.myLooper() != looper) { - handler.post(new Runnable() { - @Override - public void run() { - register(resource); - } - }); - } else { - for (IdlingResource oldResource : resources) { - if (resource.getName().equals(oldResource.getName())) { - // This does not throw an error to avoid leaving tests that register resource in test - // setup in an undeterministic state (we cannot assume that everyone clears vm state - // between each test run) - Log.e(TAG, String.format("Attempted to register resource with same names:" + - " %s. R1: %s R2: %s.\nDuplicate resource registration will be ignored.", - resource.getName(), resource, oldResource)); - return; - } - } - resources.add(resource); - final int position = resources.size() - 1; - registerToIdleCallback(resource, position); - idleState.set(position, resource.isIdleNow()); - } - } - - public void registerLooper(Looper looper, boolean considerWaitIdle) { - checkNotNull(looper); - checkArgument(Looper.getMainLooper() != looper, "Not intended for use with main looper!"); - register(new LooperIdlingResource(looper, considerWaitIdle)); - } - - private void registerToIdleCallback(IdlingResource resource, final int position) { - resource.registerIdleTransitionCallback(new ResourceCallback() { - @Override - public void onTransitionToIdle() { - Message m = handler.obtainMessage(DYNAMIC_RESOURCE_HAS_IDLED); - m.arg1 = position; - handler.sendMessage(m); - } - }); - } - - boolean allResourcesAreIdle() { - checkState(Looper.myLooper() == looper); - for (int i = idleState.nextSetBit(0); i >= 0 && i < resources.size(); - i = idleState.nextSetBit(i + 1)) { - idleState.set(i, resources.get(i).isIdleNow()); - } - return idleState.cardinality() == resources.size(); - } - - interface IdleNotificationCallback { - public void allResourcesIdle(); - - public void resourcesStillBusyWarning(List<String> busyResourceNames); - - public void resourcesHaveTimedOut(List<String> busyResourceNames); - } - - void notifyWhenAllResourcesAreIdle(IdleNotificationCallback callback) { - checkNotNull(callback); - checkState(Looper.myLooper() == looper); - checkState(idleNotificationCallback == NO_OP_CALLBACK, "Callback has already been registered."); - if (allResourcesAreIdle()) { - callback.allResourcesIdle(); - } else { - idleNotificationCallback = callback; - scheduleTimeoutMessages(); - } - } - - void cancelIdleMonitor() { - dispatcher.deregister(); - } - - private void scheduleTimeoutMessages() { - IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy(); - Message timeoutWarning = handler.obtainMessage(IDLE_WARNING_REACHED, TIMEOUT_MESSAGE_TAG); - handler.sendMessageDelayed(timeoutWarning, warning.getIdleTimeoutUnit().toMillis( - warning.getIdleTimeout())); - Message timeoutError = handler.obtainMessage(TIMEOUT_OCCURRED, TIMEOUT_MESSAGE_TAG); - IdlingPolicy error = IdlingPolicies.getDynamicIdlingResourceErrorPolicy(); - - handler.sendMessageDelayed(timeoutError, error.getIdleTimeoutUnit().toMillis( - error.getIdleTimeout())); - } - - private List<String> getBusyResources() { - List<String> busyResourceNames = Lists.newArrayList(); - List<Integer> racyResources = Lists.newArrayList(); - - for (int i = 0; i < resources.size(); i++) { - IdlingResource resource = resources.get(i); - if (!idleState.get(i)) { - if (resource.isIdleNow()) { - // We have not been notified of a BUSY -> IDLE transition, but the resource is telling us - // its that its idle. Either it's a race condition or is this resource buggy. - racyResources.add(i); - } else { - busyResourceNames.add(resource.getName()); - } - } - } - - if (!racyResources.isEmpty()) { - Message raceBuster = handler.obtainMessage(POSSIBLE_RACE_CONDITION_DETECTED, - TIMEOUT_MESSAGE_TAG); - raceBuster.obj = racyResources; - handler.sendMessage(raceBuster); - return null; - } else { - return busyResourceNames; - } - } - - - private class Dispatcher implements Handler.Callback { - @Override - public boolean handleMessage(Message m) { - switch (m.what) { - case DYNAMIC_RESOURCE_HAS_IDLED: - handleResourceIdled(m); - break; - case IDLE_WARNING_REACHED: - handleTimeoutWarning(); - break; - case TIMEOUT_OCCURRED: - handleTimeout(); - break; - case POSSIBLE_RACE_CONDITION_DETECTED: - handleRaceCondition(m); - break; - default: - Log.w(TAG, "Unknown message type: " + m); - return false; - } - return true; - } - - private void handleResourceIdled(Message m) { - idleState.set(m.arg1, true); - if (idleState.cardinality() == resources.size()) { - try { - idleNotificationCallback.allResourcesIdle(); - } finally { - deregister(); - } - } - } - - private void handleTimeoutWarning() { - List<String> busyResources = getBusyResources(); - if (busyResources == null) { - // null indicates that there is either a race or a programming error - // a race detector message has been inserted into the q. - // reinsert the idle_warning_reached message into the q directly after it - // so we generate warnings if the system is still sane. - handler.sendMessage(handler.obtainMessage(IDLE_WARNING_REACHED, TIMEOUT_MESSAGE_TAG)); - } else { - IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy(); - idleNotificationCallback.resourcesStillBusyWarning(busyResources); - handler.sendMessageDelayed( - handler.obtainMessage(IDLE_WARNING_REACHED, TIMEOUT_MESSAGE_TAG), - warning.getIdleTimeoutUnit().toMillis(warning.getIdleTimeout())); - } - } - - private void handleTimeout() { - List<String> busyResources = getBusyResources(); - if (busyResources == null) { - // detected a possible race... we've enqueued a race busting message - // so either that'll resolve the race or kill the app because it's buggy. - // if the race resolves, we need to timeout properly. - handler.sendMessage(handler.obtainMessage(TIMEOUT_OCCURRED, TIMEOUT_MESSAGE_TAG)); - } else { - try { - idleNotificationCallback.resourcesHaveTimedOut(busyResources); - } finally { - deregister(); - } - } - } - - @SuppressWarnings("unchecked") - private void handleRaceCondition(Message m) { - for (Integer i : (List<Integer>) m.obj) { - if (idleState.get(i)) { - // it was a race... i is now idle, everything is fine... - } else { - throw new IllegalStateException(String.format( - "Resource %s isIdleNow() is returning true, but a message indicating that the " - + "resource has transitioned from busy to idle was never sent.", - resources.get(i).getName())); - } - } - } - - private void deregister() { - handler.removeCallbacksAndMessages(TIMEOUT_MESSAGE_TAG); - idleNotificationCallback = NO_OP_CALLBACK; - } - } -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/InputManagerEventInjectionStrategy.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/InputManagerEventInjectionStrategy.java deleted file mode 100644 index d324795..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/InputManagerEventInjectionStrategy.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Throwables.propagate; - -import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException; - -import android.os.Build; -import android.util.Log; -import android.view.InputDevice; -import android.view.InputEvent; -import android.view.KeyEvent; -import android.view.MotionEvent; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * An {@link EventInjectionStrategy} that uses the input manager to inject Events. - * This strategy supports API level 16 and above. - */ -final class InputManagerEventInjectionStrategy implements EventInjectionStrategy { - private static final String TAG = InputManagerEventInjectionStrategy.class.getSimpleName(); - - // Used in reflection - private boolean initComplete; - private Method injectInputEventMethod; - private Method setSourceMotionMethod; - private Object instanceInputManagerObject; - private int motionEventMode; - private int keyEventMode; - - InputManagerEventInjectionStrategy() { - checkState(Build.VERSION.SDK_INT >= 16, "Unsupported API level."); - } - - void initialize() { - if (initComplete) { - return; - } - - try { - Log.d(TAG, "Creating injection strategy with input manager."); - - // Get the InputputManager class object and initialize if necessary. - Class<?> inputManagerClassObject = Class.forName("android.hardware.input.InputManager"); - Method getInstanceMethod = inputManagerClassObject.getDeclaredMethod("getInstance"); - getInstanceMethod.setAccessible(true); - - instanceInputManagerObject = getInstanceMethod.invoke(inputManagerClassObject); - - injectInputEventMethod = instanceInputManagerObject.getClass() - .getDeclaredMethod("injectInputEvent", InputEvent.class, Integer.TYPE); - injectInputEventMethod.setAccessible(true); - - // Setting event mode to INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH to ensure - // that we've dispatched the event and any side effects its had on the view hierarchy - // have occurred. - Field motionEventModeField = - inputManagerClassObject.getField("INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH"); - motionEventModeField.setAccessible(true); - motionEventMode = motionEventModeField.getInt(inputManagerClassObject); - - Field keyEventModeField = - inputManagerClassObject.getField("INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH"); - keyEventModeField.setAccessible(true); - keyEventMode = keyEventModeField.getInt(inputManagerClassObject); - - setSourceMotionMethod = MotionEvent.class.getDeclaredMethod("setSource", Integer.TYPE); - InputEvent.class.getDeclaredMethod("getSequenceNumber"); - initComplete = true; - } catch (ClassNotFoundException e) { - propagate(e); - } catch (IllegalAccessException e) { - propagate(e); - } catch (IllegalArgumentException e) { - propagate(e); - } catch (InvocationTargetException e) { - propagate(e); - } catch (NoSuchMethodException e) { - propagate(e); - } catch (SecurityException e) { - propagate(e); - } catch (NoSuchFieldException e) { - propagate(e); - } - } - - @Override - public boolean injectKeyEvent(KeyEvent keyEvent) throws InjectEventSecurityException { - try { - return (Boolean) injectInputEventMethod.invoke(instanceInputManagerObject, - keyEvent, keyEventMode); - } catch (IllegalAccessException e) { - propagate(e); - } catch (IllegalArgumentException e) { - propagate(e); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - if (cause instanceof SecurityException) { - throw new InjectEventSecurityException(cause); - } - propagate(e); - } catch (SecurityException e) { - throw new InjectEventSecurityException(e); - } - return false; - } - - @Override - public boolean injectMotionEvent(MotionEvent motionEvent) throws InjectEventSecurityException { - try { - // Need to set the event source to touch screen, otherwise the input can be ignored even - // though injecting it would be successful. - // TODO(user): proper handling of events from a trackball (SOURCE_TRACKBALL) and joystick. - if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0 - && !isFromTouchpadInGlassDevice(motionEvent)) { - // Need to do runtime invocation of setSource because it was not added until 2.3_r1. - setSourceMotionMethod.invoke(motionEvent, InputDevice.SOURCE_TOUCHSCREEN); - } - return (Boolean) injectInputEventMethod.invoke(instanceInputManagerObject, - motionEvent, motionEventMode); - } catch (IllegalAccessException e) { - propagate(e); - } catch (IllegalArgumentException e) { - propagate(e); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - if (cause instanceof SecurityException) { - throw new InjectEventSecurityException(cause); - } - propagate(e); - } catch (SecurityException e) { - throw new InjectEventSecurityException(e); - } - return false; - } - - // We'd like to inject non-pointer events sourced from touchpad in Glass. - private static boolean isFromTouchpadInGlassDevice(MotionEvent motionEvent) { - return (Build.DEVICE.contains("glass") - || Build.DEVICE.contains("Glass") || Build.DEVICE.contains("wingman")) - && ((motionEvent.getSource() & InputDevice.SOURCE_TOUCHPAD) != 0); - } -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/LooperIdlingResource.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/LooperIdlingResource.java deleted file mode 100644 index b75fd36..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/LooperIdlingResource.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import com.google.android.apps.common.testing.ui.espresso.IdlingResource; -import com.google.android.apps.common.testing.ui.espresso.IdlingResource.ResourceCallback; -import com.google.android.apps.common.testing.ui.espresso.base.QueueInterrogator.QueueState; - -import android.os.Handler; -import android.os.Looper; -import android.os.MessageQueue.IdleHandler; - -/** - * An Idling Resource Adapter for Loopers. - */ -final class LooperIdlingResource implements IdlingResource { - - private static final String TAG = "LooperIdleResource"; - - private final boolean considerWaitIdle; - private final Looper monitoredLooper; - private final Handler monitoredHandler; - - private ResourceCallback resourceCallback; - - LooperIdlingResource(Looper monitoredLooper, boolean considerWaitIdle) { - this.monitoredLooper = checkNotNull(monitoredLooper); - this.monitoredHandler = new Handler(monitoredLooper); - this.considerWaitIdle = considerWaitIdle; - checkState(Looper.getMainLooper() != monitoredLooper, "Not for use with main looper."); - } - - // Only assigned and read from the main loop. - private QueueInterrogator queueInterrogator; - - @Override - public String getName() { - return monitoredLooper.getThread().getName(); - } - - @Override - public boolean isIdleNow() { - // on main thread here. - QueueState state = queueInterrogator.determineQueueState(); - boolean idle = state == QueueState.EMPTY || state == QueueState.TASK_DUE_LONG; - boolean idleWait = considerWaitIdle - && monitoredLooper.getThread().getState() == Thread.State.WAITING; - if (idleWait) { - if (resourceCallback != null) { - resourceCallback.onTransitionToIdle(); - } - } - return idle || idleWait; - } - - @Override - public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { - this.resourceCallback = resourceCallback; - // on main thread here. - queueInterrogator = new QueueInterrogator(monitoredLooper); - - // must load idle handlers from monitored looper thread. - IdleHandler idleHandler = new ResourceCallbackIdleHandler(resourceCallback, queueInterrogator, - monitoredHandler); - - checkState(monitoredHandler.postAtFrontOfQueue(new Initializer(idleHandler)), - "Monitored looper exiting."); - } - - private static class ResourceCallbackIdleHandler implements IdleHandler { - private final ResourceCallback resourceCallback; - private final QueueInterrogator myInterrogator; - private final Handler myHandler; - - ResourceCallbackIdleHandler(ResourceCallback resourceCallback, - QueueInterrogator myInterrogator, Handler myHandler) { - this.resourceCallback = checkNotNull(resourceCallback); - this.myInterrogator = checkNotNull(myInterrogator); - this.myHandler = checkNotNull(myHandler); - } - - @Override - public boolean queueIdle() { - // invoked on the monitored looper thread. - QueueState queueState = myInterrogator.determineQueueState(); - if (queueState == QueueState.EMPTY || queueState == QueueState.TASK_DUE_LONG) { - // no block and no task coming 'shortly'. - resourceCallback.onTransitionToIdle(); - } else if (queueState == QueueState.BARRIER) { - // send a sentinal message that'll cause us to queueIdle again once the - // block is lifted. - myHandler.sendEmptyMessage(-1); - } - - return true; - } - } - - private static class Initializer implements Runnable { - private final IdleHandler myIdleHandler; - - Initializer(IdleHandler myIdleHandler) { - this.myIdleHandler = checkNotNull(myIdleHandler); - } - - @Override - public void run() { - // on monitored looper thread. - Looper.myQueue().addIdleHandler(myIdleHandler); - } - } - -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/MainThread.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/MainThread.java deleted file mode 100644 index c431f48..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/MainThread.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.inject.Qualifier; - -/** - * Annotates an Executor that executes tasks on the main thread - */ -@Qualifier -@Retention(RetentionPolicy.RUNTIME) -public @interface MainThread { } diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/QueueInterrogator.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/QueueInterrogator.java deleted file mode 100644 index cd63eb8..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/QueueInterrogator.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Throwables.propagate; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.MessageQueue; -import android.os.SystemClock; -import android.util.Log; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; - -/** - * Isolates the nasty details of touching the message queue. - */ -final class QueueInterrogator { - - enum QueueState { EMPTY, TASK_DUE_SOON, TASK_DUE_LONG, BARRIER }; - - private static final String TAG = "QueueInterrogator"; - - private static final Method messageQueueNextMethod; - private static final Field messageQueueHeadField; - private static final int LOOKAHEAD_MILLIS = 15; - - private final Looper interrogatedLooper; - private volatile MessageQueue interrogatedQueue; - - static { - Method nextMethod = null; - Field headField = null; - try { - nextMethod = MessageQueue.class.getDeclaredMethod("next"); - nextMethod.setAccessible(true); - - headField = MessageQueue.class.getDeclaredField("mMessages"); - headField.setAccessible(true); - } catch (IllegalArgumentException e) { - nextMethod = null; - headField = null; - Log.e(TAG, "Could not initialize interrogator!", e); - } catch (NoSuchFieldException e) { - nextMethod = null; - headField = null; - Log.e(TAG, "Could not initialize interrogator!", e); - } catch (NoSuchMethodException e) { - nextMethod = null; - headField = null; - Log.e(TAG, "Could not initialize interrogator!", e); - } catch (SecurityException e) { - nextMethod = null; - headField = null; - Log.e(TAG, "Could not initialize interrogator!", e); - } finally { - messageQueueNextMethod = nextMethod; - messageQueueHeadField = headField; - } - } - - QueueInterrogator(Looper interrogatedLooper) { - this.interrogatedLooper = checkNotNull(interrogatedLooper); - checkNotNull(messageQueueHeadField); - checkNotNull(messageQueueNextMethod); - } - - // Only for use by espresso - keep package private. - Message getNextMessage() { - checkThread(); - - if (null == interrogatedQueue) { - initializeQueue(); - } - - try { - return (Message) messageQueueNextMethod.invoke(Looper.myQueue()); - } catch (IllegalAccessException e) { - throw propagate(e); - } catch (IllegalArgumentException e) { - throw propagate(e); - } catch (InvocationTargetException e) { - throw propagate(e); - } catch (SecurityException e) { - throw propagate(e); - } - } - - QueueState determineQueueState() { - // may be called from any thread. - - if (null == interrogatedQueue) { - initializeQueue(); - } - synchronized (interrogatedQueue) { - try { - Message head = (Message) messageQueueHeadField.get(interrogatedQueue); - if (null == head) { - // no messages pending - AT ALL! - return QueueState.EMPTY; - } - if (null == head.getTarget()) { - // null target is a sync barrier token. - return QueueState.BARRIER; - } else { - long headWhen = head.getWhen(); - long nowFuz = SystemClock.uptimeMillis() + LOOKAHEAD_MILLIS; - - if (nowFuz > headWhen) { - return QueueState.TASK_DUE_SOON; - } else { - return QueueState.TASK_DUE_LONG; - } - } - } catch (IllegalAccessException e) { - throw propagate(e); - } - } - } - - private void initializeQueue() { - if (interrogatedLooper == Looper.myLooper()) { - interrogatedQueue = Looper.myQueue(); - } else { - Handler oneShotHandler = new Handler(interrogatedLooper); - FutureTask<MessageQueue> queueCapture = new FutureTask<MessageQueue>( - new Callable<MessageQueue>() { - @Override - public MessageQueue call() { - return Looper.myQueue(); - } - }); - oneShotHandler.postAtFrontOfQueue(queueCapture); - try { - interrogatedQueue = queueCapture.get(); - } catch (ExecutionException ee) { - throw propagate(ee.getCause()); - } catch (InterruptedException ie) { - throw propagate(ie); - } - } - } - - private void checkThread() { - checkState(interrogatedLooper == Looper.myLooper(), "Calling from non-owning thread!"); - } -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootViewPicker.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootViewPicker.java deleted file mode 100644 index 6870aa3..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootViewPicker.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.android.apps.common.testing.ui.espresso.matcher.RootMatchers.isDialog; -import static com.google.android.apps.common.testing.ui.espresso.matcher.RootMatchers.isFocusable; -import static com.google.common.base.Preconditions.checkState; - -import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitor; -import com.google.android.apps.common.testing.testrunner.Stage; -import com.google.android.apps.common.testing.ui.espresso.NoActivityResumedException; -import com.google.android.apps.common.testing.ui.espresso.NoMatchingRootException; -import com.google.android.apps.common.testing.ui.espresso.Root; -import com.google.android.apps.common.testing.ui.espresso.UiController; -import com.google.common.base.Joiner; -import com.google.common.collect.Lists; - -import android.app.Activity; -import android.os.Looper; -import android.util.Log; -import android.view.View; - -import org.hamcrest.Matcher; - -import java.util.Collection; -import java.util.EnumSet; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import javax.inject.Inject; -import javax.inject.Provider; -import javax.inject.Singleton; - -/** - * Provides the root View of the top-most Window, with which the user can interact. View is - * guaranteed to be in a stable state - i.e. not pending any updates from the application. - * - * This provider can only be accessed from the main thread. - */ -@Singleton -public final class RootViewPicker implements Provider<View> { - private static final String TAG = RootViewPicker.class.getSimpleName(); - - private final Provider<List<Root>> rootsOracle; - private final UiController uiController; - private final ActivityLifecycleMonitor activityLifecycleMonitor; - private final AtomicReference<Matcher<Root>> rootMatcherRef; - - private List<Root> roots; - - @Inject - RootViewPicker(Provider<List<Root>> rootsOracle, UiController uiController, - ActivityLifecycleMonitor activityLifecycleMonitor, - AtomicReference<Matcher<Root>> rootMatcherRef) { - this.rootsOracle = rootsOracle; - this.uiController = uiController; - this.activityLifecycleMonitor = activityLifecycleMonitor; - this.rootMatcherRef = rootMatcherRef; - } - - @Override - public View get() { - checkState(Looper.getMainLooper().equals(Looper.myLooper()), "must be called on main thread."); - Matcher<Root> rootMatcher = rootMatcherRef.get(); - - Root root = findRoot(rootMatcher); - - // we only want to propagate a root view that the user can interact with and is not - // about to relay itself out. An app should be in this state the majority of the time, - // if we happen not to be in this state at the moment, process the queue some more - // we should come to it quickly enough. - int loops = 0; - - while (!isReady(root)) { - if (loops < 3) { - uiController.loopMainThreadUntilIdle(); - } else if (loops < 1001) { - - // loopUntil idle effectively is polling and pegs the CPU... if we don't have an update to - // process immediately, we might have something coming very very soon. - uiController.loopMainThreadForAtLeast(10); - } else { - // we've waited for the root view to be fully laid out and have window focus - // for over 10 seconds. something is wrong. - throw new RuntimeException(String.format("Waited for the root of the view hierarchy to have" - + " window focus and not be requesting layout for over 10 seconds. If you specified a" - + " non default root matcher, it may be picking a root that never takes focus." - + " Otherwise, something is seriously wrong. Selected Root:\n%s\n. All Roots:\n%s" - , root, Joiner.on("\n").join(roots))); - } - - root = findRoot(rootMatcher); - loops++; - } - - return root.getDecorView(); - } - - private boolean isReady(Root root) { - // Root is ready (i.e. UI is no longer in flux) if layout of the root view is not being - // requested and the root view has window focus (if it is focusable). - View rootView = root.getDecorView(); - if (!rootView.isLayoutRequested()) { - return rootView.hasWindowFocus() || !isFocusable().matches(root); - } - return false; - } - - private Root findRoot(Matcher<Root> rootMatcher) { - waitForAtLeastOneActivityToBeResumed(); - - roots = rootsOracle.get(); - - // TODO(user): move these checks into the RootsOracle. - if (roots.isEmpty()) { - // Reflection broke - throw new RuntimeException("No root window were discovered."); - } - - if (roots.size() > 1) { - // Multiple roots only occur: - // when multiple activities are in some state of their lifecycle in the application - // - we don't care about this, since we only want to interact with the RESUMED - // activity, all other activities windows are not visible to the user so, out of - // scope. - // when a PopupWindow or PopupMenu is used - // - this is a case where we definitely want to consider the top most window, since - // it probably has the most useful info in it. - // when an android.app.dialog is shown - // - again, this is getting all the users attention, so it gets the test attention - // too. - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, String.format("Multiple windows detected: %s", roots)); - } - } - - List<Root> selectedRoots = Lists.newArrayList(); - for (Root root : roots) { - if (rootMatcher.matches(root)) { - selectedRoots.add(root); - } - } - - if (selectedRoots.isEmpty()) { - throw NoMatchingRootException.create(rootMatcher, roots); - } - - return reduceRoots(selectedRoots); - } - - @SuppressWarnings("unused") - private void waitForAtLeastOneActivityToBeResumed() { - Collection<Activity> resumedActivities = - activityLifecycleMonitor.getActivitiesInStage(Stage.RESUMED); - if (resumedActivities.isEmpty()) { - uiController.loopMainThreadUntilIdle(); - resumedActivities = activityLifecycleMonitor.getActivitiesInStage(Stage.RESUMED); - } - if (resumedActivities.isEmpty()) { - List<Activity> activities = Lists.newArrayList(); - for (Stage s : EnumSet.range(Stage.PRE_ON_CREATE, Stage.RESTARTED)) { - activities.addAll(activityLifecycleMonitor.getActivitiesInStage(s)); - } - if (activities.isEmpty()) { - throw new RuntimeException("No activities found. Did you forget to launch the activity " - + "by calling getActivity() or startActivitySync or similar?"); - } - // well at least there are some activities in the pipeline - lets see if they resume. - - long[] waitTimes = - {10, 50, 100, 500, TimeUnit.SECONDS.toMillis(2), TimeUnit.SECONDS.toMillis(30)}; - - for (int waitIdx = 0; waitIdx < waitTimes.length; waitIdx++) { - Log.w(TAG, "No activity currently resumed - waiting: " + waitTimes[waitIdx] - + "ms for one to appear."); - uiController.loopMainThreadForAtLeast(waitTimes[waitIdx]); - resumedActivities = activityLifecycleMonitor.getActivitiesInStage(Stage.RESUMED); - if (!resumedActivities.isEmpty()) { - return; // one of the pending activities has resumed - } - } - throw new NoActivityResumedException("No activities in stage RESUMED. Did you forget to " - + "launch the activity. (test.getActivity() or similar)?"); - } - } - - private Root reduceRoots(List<Root> subpanels) { - Root topSubpanel = subpanels.get(0); - if (subpanels.size() >= 1) { - for (Root subpanel : subpanels) { - if (isDialog().matches(subpanel)) { - return subpanel; - } - if (subpanel.getWindowLayoutParams().get().type - > topSubpanel.getWindowLayoutParams().get().type) { - topSubpanel = subpanel; - } - } - } - return topSubpanel; - } -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootsOracle.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootsOracle.java deleted file mode 100644 index a284ede..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootsOracle.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.common.base.Preconditions.checkState; - -import com.google.android.apps.common.testing.ui.espresso.Root; -import com.google.common.collect.Lists; - -import android.os.Build; -import android.os.Looper; -import android.util.Log; -import android.view.View; -import android.view.WindowManager.LayoutParams; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Provider; -import javax.inject.Singleton; - -/** - * Provides access to all root views in an application. - * - * 95% of the time this is unnecessary and we can operate solely on current Activity's root view - * as indicated by getWindow().getDecorView(). However in the case of popup windows, menus, and - * dialogs the actual view hierarchy we should be operating on is not accessible thru public apis. - * - * In the spirit of degrading gracefully when new api levels break compatibility, callers should - * handle a list of size 0 by assuming getWindow().getDecorView() on the currently resumed activity - * is the sole root - this assumption will be correct often enough. - * - * Obviously, you need to be on the main thread to use this. - */ -@Singleton -final class RootsOracle implements Provider<List<Root>> { - - private static final String TAG = RootsOracle.class.getSimpleName(); - private static final String WINDOW_MANAGER_IMPL_CLAZZ = - "android.view.WindowManagerImpl"; - private static final String WINDOW_MANAGER_GLOBAL_CLAZZ = - "android.view.WindowManagerGlobal"; - private static final String VIEWS_FIELD = "mViews"; - private static final String WINDOW_PARAMS_FIELD = "mParams"; - private static final String GET_DEFAULT_IMPL = "getDefault"; - private static final String GET_GLOBAL_INSTANCE = "getInstance"; - - private final Looper mainLooper; - private boolean initialized; - private Object windowManagerObj; - private Field viewsField; - private Field paramsField; - - @Inject - RootsOracle(Looper mainLooper) { - this.mainLooper = mainLooper; - } - - @SuppressWarnings("unchecked") - @Override - public List<Root> get() { - checkState(mainLooper.equals(Looper.myLooper()), "must be called on main thread."); - - if (!initialized) { - initialize(); - } - - if (null == windowManagerObj) { - Log.w(TAG, "No reflective access to windowmanager object."); - return Lists.newArrayList(); - } - - if (null == viewsField) { - Log.w(TAG, "No reflective access to mViews"); - return Lists.newArrayList(); - } - if (null == paramsField) { - Log.w(TAG, "No reflective access to mPArams"); - return Lists.newArrayList(); - } - - List<View> views = null; - List<LayoutParams> params = null; - - try { - if (Build.VERSION.SDK_INT < 19) { - views = Arrays.asList((View[]) viewsField.get(windowManagerObj)); - params = Arrays.asList((LayoutParams[]) paramsField.get(windowManagerObj)); - } else { - views = (List<View>) viewsField.get(windowManagerObj); - params = (List<LayoutParams>) paramsField.get(windowManagerObj); - } - } catch (RuntimeException re) { - Log.w(TAG, String.format("Reflective access to %s or %s on %s failed.", - viewsField, paramsField, windowManagerObj), re); - return Lists.newArrayList(); - } catch (IllegalAccessException iae) { - Log.w(TAG, String.format("Reflective access to %s or %s on %s failed.", - viewsField, paramsField, windowManagerObj), iae); - return Lists.newArrayList(); - } - - - List<Root> roots = Lists.newArrayList(); - for (int i = views.size() - 1; i > -1; i--) { - roots.add( - new Root.Builder() - .withDecorView(views.get(i)) - .withWindowLayoutParams(params.get(i)) - .build()); - } - - return roots; - } - - private void initialize() { - initialized = true; - String accessClass = Build.VERSION.SDK_INT > 16 ? WINDOW_MANAGER_GLOBAL_CLAZZ - : WINDOW_MANAGER_IMPL_CLAZZ; - String instanceMethod = Build.VERSION.SDK_INT > 16 ? GET_GLOBAL_INSTANCE : GET_DEFAULT_IMPL; - - try { - Class<?> clazz = Class.forName(accessClass); - Method getMethod = clazz.getMethod(instanceMethod); - windowManagerObj = getMethod.invoke(null); - viewsField = clazz.getDeclaredField(VIEWS_FIELD); - viewsField.setAccessible(true); - paramsField = clazz.getDeclaredField(WINDOW_PARAMS_FIELD); - paramsField.setAccessible(true); - } catch (InvocationTargetException ite) { - Log.e(TAG, String.format("could not invoke: %s on %s", instanceMethod, accessClass), - ite.getCause()); - } catch (ClassNotFoundException cnfe) { - Log.e(TAG, String.format("could not find class: %s", accessClass), cnfe); - } catch (NoSuchFieldException nsfe) { - Log.e(TAG, String.format("could not find field: %s or %s on %s", WINDOW_PARAMS_FIELD, - VIEWS_FIELD, accessClass), nsfe); - } catch (NoSuchMethodException nsme) { - Log.e(TAG, String.format("could not find method: %s on %s", instanceMethod, accessClass), - nsme); - } catch (RuntimeException re) { - Log.e(TAG, String.format("reflective setup failed using obj: %s method: %s field: %s", - accessClass, instanceMethod, VIEWS_FIELD), re); - } catch (IllegalAccessException iae) { - Log.e(TAG, String.format("reflective setup failed using obj: %s method: %s field: %s", - accessClass, instanceMethod, VIEWS_FIELD), iae); - } - } -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/SdkAsyncTask.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/SdkAsyncTask.java deleted file mode 100644 index b28255e..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/SdkAsyncTask.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.inject.Qualifier; - -/** - * Annotates a AsyncTaskMonitor as monitoring the SdkAsyncTask pool - */ -@Qualifier -@Retention(RetentionPolicy.RUNTIME) -@interface SdkAsyncTask { } diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ThreadPoolExecutorExtractor.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ThreadPoolExecutorExtractor.java deleted file mode 100644 index 1a719a3..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ThreadPoolExecutorExtractor.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import com.google.common.base.Optional; - -import android.os.Build; -import android.os.Handler; -import android.os.Looper; - -import java.lang.reflect.Field; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; -import java.util.concurrent.ThreadPoolExecutor; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Extracts ThreadPoolExecutors used by pieces of android. - * - * We do some work to ensure that we load the classes containing these thread pools - * on the main thread, since they may have static initialization that assumes access - * to the main looper. - */ -@Singleton -final class ThreadPoolExecutorExtractor { - private static final String ASYNC_TASK_CLASS_NAME = "android.os.AsyncTask"; - private static final String MODERN_ASYNC_TASK_CLASS_NAME = - "android.support.v4.content.ModernAsyncTask"; - private static final String MODERN_ASYNC_TASK_FIELD_NAME = "THREAD_POOL_EXECUTOR"; - private static final String LEGACY_ASYNC_TASK_FIELD_NAME = "sExecutor"; - private final Handler mainHandler; - - @Inject - ThreadPoolExecutorExtractor(Looper looper) { - mainHandler = new Handler(looper); - } - - - public ThreadPoolExecutor getAsyncTaskThreadPool() { - FutureTask<Optional<ThreadPoolExecutor>> getTask = null; - if (Build.VERSION.SDK_INT < 11) { - getTask = new FutureTask<Optional<ThreadPoolExecutor>>(LEGACY_ASYNC_TASK_EXECUTOR); - } else { - getTask = new FutureTask<Optional<ThreadPoolExecutor>>(POST_HONEYCOMB_ASYNC_TASK_EXECUTOR); - } - - try { - return runOnMainThread(getTask).get().get(); - } catch (InterruptedException ie) { - throw new RuntimeException("Interrupted while trying to get the async task executor!", ie); - } catch (ExecutionException ee) { - throw new RuntimeException(ee.getCause()); - } - } - - public Optional<ThreadPoolExecutor> getCompatAsyncTaskThreadPool() { - try { - return runOnMainThread( - new FutureTask<Optional<ThreadPoolExecutor>>(MODERN_ASYNC_TASK_EXTRACTOR)).get(); - } catch (InterruptedException ie) { - throw new RuntimeException("Interrupted while trying to get the compat async executor!", ie); - } catch (ExecutionException ee) { - throw new RuntimeException(ee.getCause()); - } - } - - private <T> FutureTask<T> runOnMainThread(final FutureTask<T> futureToRun) { - if (Looper.myLooper() != Looper.getMainLooper()) { - final CountDownLatch latch = new CountDownLatch(1); - mainHandler.post(new Runnable() { - @Override - public void run() { - try { - futureToRun.run(); - } finally { - latch.countDown(); - } - } - }); - try { - latch.await(); - } catch (InterruptedException ie) { - if (!futureToRun.isDone()) { - throw new RuntimeException("Interrupted while waiting for task to complete."); - } - } - } else { - futureToRun.run(); - } - - return futureToRun; - } - - private static final Callable<Optional<ThreadPoolExecutor>> MODERN_ASYNC_TASK_EXTRACTOR = - new Callable<Optional<ThreadPoolExecutor>>() { - @Override - public Optional<ThreadPoolExecutor> call() throws Exception { - try { - Class<?> modernClazz = Class.forName(MODERN_ASYNC_TASK_CLASS_NAME); - Field executorField = modernClazz.getField(MODERN_ASYNC_TASK_FIELD_NAME); - return Optional.of((ThreadPoolExecutor) executorField.get(null)); - } catch (ClassNotFoundException cnfe) { - return Optional.<ThreadPoolExecutor>absent(); - } - } - }; - - private static final Callable<Class<?>> LOAD_ASYNC_TASK_CLASS = - new Callable<Class<?>>() { - @Override - public Class<?> call() throws Exception { - return Class.forName(ASYNC_TASK_CLASS_NAME); - } - }; - - private static final Callable<Optional<ThreadPoolExecutor>> LEGACY_ASYNC_TASK_EXECUTOR = - new Callable<Optional<ThreadPoolExecutor>>() { - @Override - public Optional<ThreadPoolExecutor> call() throws Exception { - Field executorField = LOAD_ASYNC_TASK_CLASS.call() - .getDeclaredField(LEGACY_ASYNC_TASK_FIELD_NAME); - executorField.setAccessible(true); - return Optional.of((ThreadPoolExecutor) executorField.get(null)); - } - }; - - private static final Callable<Optional<ThreadPoolExecutor>> POST_HONEYCOMB_ASYNC_TASK_EXECUTOR = - new Callable<Optional<ThreadPoolExecutor>>() { - @Override - public Optional<ThreadPoolExecutor> call() throws Exception { - Field executorField = LOAD_ASYNC_TASK_CLASS.call() - .getField(MODERN_ASYNC_TASK_FIELD_NAME); - return Optional.of((ThreadPoolExecutor) executorField.get(null)); - } - }; -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImpl.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImpl.java deleted file mode 100644 index c1aaa5c..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImpl.java +++ /dev/null @@ -1,535 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Throwables.propagate; - -import com.google.android.apps.common.testing.ui.espresso.IdlingPolicies; -import com.google.android.apps.common.testing.ui.espresso.IdlingPolicy; -import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException; -import com.google.android.apps.common.testing.ui.espresso.UiController; -import com.google.android.apps.common.testing.ui.espresso.base.IdlingResourceRegistry.IdleNotificationCallback; -import com.google.android.apps.common.testing.ui.espresso.base.QueueInterrogator.QueueState; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Optional; -import com.google.common.collect.Lists; - -import android.annotation.SuppressLint; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.SystemClock; -import android.util.Log; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; -import android.view.MotionEvent; - -import java.util.BitSet; -import java.util.EnumSet; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.FutureTask; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Implementation of {@link UiController}. - */ -@Singleton -final class UiControllerImpl implements UiController, Handler.Callback { - - private static final String TAG = UiControllerImpl.class.getSimpleName(); - - private static final Callable<Void> NO_OP = new Callable<Void>() { - @Override - public Void call() { - return null; - } - }; - - /** - * Responsible for signaling a particular condition is met / verifying that signal. - */ - enum IdleCondition { - DELAY_HAS_PAST, - ASYNC_TASKS_HAVE_IDLED, - COMPAT_TASKS_HAVE_IDLED, - KEY_INJECT_HAS_COMPLETED, - MOTION_INJECTION_HAS_COMPLETED, - DYNAMIC_TASKS_HAVE_IDLED; - - /** - * Checks whether this condition has been signaled. - */ - public boolean isSignaled(BitSet conditionSet) { - return conditionSet.get(ordinal()); - } - - /** - * Resets the signal state for this condition. - */ - public void reset(BitSet conditionSet) { - conditionSet.set(ordinal(), false); - } - - /** - * Creates a message that when sent will raise the signal of this condition. - */ - public Message createSignal(Handler handler, int myGeneration) { - return Message.obtain(handler, ordinal(), myGeneration, 0, null); - } - - /** - * Handles a message that is raising a signal and updates the condition set accordingly. - * Messages from a previous generation will be ignored. - */ - public static boolean handleMessage(Message message, BitSet conditionSet, - int currentGeneration) { - IdleCondition [] allConditions = values(); - if (message.what < 0 || message.what >= allConditions.length) { - return false; - } else { - IdleCondition condition = allConditions[message.what]; - if (message.arg1 == currentGeneration) { - condition.signal(conditionSet); - } else { - Log.w(TAG, "ignoring signal of: " + condition + " from previous generation: " + - message.arg1 + " current generation: " + currentGeneration); - } - return true; - } - } - - public static BitSet createConditionSet() { - return new BitSet(values().length); - } - - /** - * Requests that the given bitset be updated to indicate that this condition has been - * signaled. - */ - protected void signal(BitSet conditionSet) { - conditionSet.set(ordinal()); - } - } - - private final EventInjector eventInjector; - private final BitSet conditionSet; - private final AsyncTaskPoolMonitor asyncTaskMonitor; - private final Optional<AsyncTaskPoolMonitor> compatTaskMonitor; - private final IdlingResourceRegistry idlingResourceRegistry; - private final ExecutorService keyEventExecutor = Executors.newSingleThreadExecutor(); - private final QueueInterrogator queueInterrogator; - private final Looper mainLooper; - - private Handler controllerHandler; - // only updated on main thread. - private boolean looping = false; - private int generation = 0; - - @VisibleForTesting - @Inject - UiControllerImpl(EventInjector eventInjector, - @SdkAsyncTask AsyncTaskPoolMonitor asyncTaskMonitor, - @CompatAsyncTask Optional<AsyncTaskPoolMonitor> compatTaskMonitor, - IdlingResourceRegistry registry, - Looper mainLooper) { - this.eventInjector = checkNotNull(eventInjector); - this.asyncTaskMonitor = checkNotNull(asyncTaskMonitor); - this.compatTaskMonitor = checkNotNull(compatTaskMonitor); - this.conditionSet = IdleCondition.createConditionSet(); - this.idlingResourceRegistry = checkNotNull(registry); - this.mainLooper = checkNotNull(mainLooper); - this.queueInterrogator = new QueueInterrogator(mainLooper); - } - - @SuppressWarnings("deprecation") - @Override - public boolean injectKeyEvent(final KeyEvent event) throws InjectEventSecurityException { - checkNotNull(event); - checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!"); - initialize(); - loopMainThreadUntilIdle(); - - FutureTask<Boolean> injectTask = new SignalingTask<Boolean>( - new Callable<Boolean>() { - @Override - public Boolean call() throws Exception { - return eventInjector.injectKeyEvent(event); - } - }, - IdleCondition.KEY_INJECT_HAS_COMPLETED, - generation); - - // Inject the key event. - keyEventExecutor.submit(injectTask); - - loopUntil(IdleCondition.KEY_INJECT_HAS_COMPLETED); - - try { - checkState(injectTask.isDone(), "Key injection was signaled - but it wasnt done."); - return injectTask.get(); - } catch (ExecutionException ee) { - if (ee.getCause() instanceof InjectEventSecurityException) { - throw (InjectEventSecurityException) ee.getCause(); - } else { - throw new RuntimeException(ee.getCause()); - } - } catch (InterruptedException neverHappens) { - // we only call get() after done() is signaled. - // we should never block. - throw new RuntimeException("impossible.", neverHappens); - } - } - - @Override - public boolean injectMotionEvent(final MotionEvent event) throws InjectEventSecurityException { - checkNotNull(event); - checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!"); - initialize(); - - FutureTask<Boolean> injectTask = new SignalingTask<Boolean>( - new Callable<Boolean>() { - @Override - public Boolean call() throws Exception { - return eventInjector.injectMotionEvent(event); - } - }, - IdleCondition.MOTION_INJECTION_HAS_COMPLETED, - generation); - keyEventExecutor.submit(injectTask); - loopUntil(IdleCondition.MOTION_INJECTION_HAS_COMPLETED); - try { - checkState(injectTask.isDone(), "Key injection was signaled - but it wasnt done."); - return injectTask.get(); - } catch (ExecutionException ee) { - if (ee.getCause() instanceof InjectEventSecurityException) { - throw (InjectEventSecurityException) ee.getCause(); - } else { - throw propagate(ee.getCause() != null ? ee.getCause() : ee); - } - } catch (InterruptedException neverHappens) { - // we only call get() after done() is signaled. - // we should never block. - throw propagate(neverHappens); - } finally { - loopMainThreadUntilIdle(); - } - } - - @Override - public boolean injectString(String str) throws InjectEventSecurityException { - checkNotNull(str); - checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!"); - initialize(); - - // No-op if string is empty. - if (str.length() == 0) { - Log.w(TAG, "Supplied string is empty resulting in no-op (nothing is typed)."); - return true; - } - - boolean eventInjected = false; - KeyCharacterMap keyCharacterMap = getKeyCharacterMap(); - - // TODO(user): Investigate why not use (as suggested in javadoc of keyCharacterMap.getEvents): - // http://developer.android.com/reference/android/view/KeyEvent.html#KeyEvent(long, - // java.lang.String, int, int) - KeyEvent[] events = keyCharacterMap.getEvents(str.toCharArray()); - checkNotNull(events, "Failed to get events for string " + str); - Log.d(TAG, String.format("Injecting string: \"%s\"", str)); - - for (KeyEvent event : events) { - checkNotNull(event, String.format("Failed to get event for character (%c) with key code (%s)", - event.getKeyCode(), event.getUnicodeChar())); - - eventInjected = false; - for (int attempts = 0; !eventInjected && attempts < 4; attempts++) { - attempts++; - - // We have to change the time of an event before injecting it because - // all KeyEvents returned by KeyCharacterMap.getEvents() have the same - // time stamp and the system rejects too old events. Hence, it is - // possible for an event to become stale before it is injected if it - // takes too long to inject the preceding ones. - event = KeyEvent.changeTimeRepeat(event, SystemClock.uptimeMillis(), 0); - eventInjected = injectKeyEvent(event); - } - - if (!eventInjected) { - Log.e(TAG, String.format("Failed to inject event for character (%c) with key code (%s)", - event.getUnicodeChar(), event.getKeyCode())); - break; - } - } - - return eventInjected; - } - - @SuppressLint("InlinedApi") - @VisibleForTesting - @SuppressWarnings("deprecation") - public static KeyCharacterMap getKeyCharacterMap() { - KeyCharacterMap keyCharacterMap = null; - - // KeyCharacterMap.VIRTUAL_KEYBOARD is present from API11. - // For earlier APIs we use KeyCharacterMap.BUILT_IN_KEYBOARD - if (Build.VERSION.SDK_INT < 11) { - keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD); - } else { - keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); - } - return keyCharacterMap; - } - - - @Override - public void loopMainThreadUntilIdle() { - initialize(); - checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!"); - do { - EnumSet<IdleCondition> condChecks = EnumSet.noneOf(IdleCondition.class); - if (!asyncTaskMonitor.isIdleNow()) { - asyncTaskMonitor.notifyWhenIdle(new SignalingTask<Void>(NO_OP, - IdleCondition.ASYNC_TASKS_HAVE_IDLED, generation)); - - condChecks.add(IdleCondition.ASYNC_TASKS_HAVE_IDLED); - } - - if (!compatIdle()) { - compatTaskMonitor.get().notifyWhenIdle(new SignalingTask<Void>(NO_OP, - IdleCondition.COMPAT_TASKS_HAVE_IDLED, generation)); - condChecks.add(IdleCondition.COMPAT_TASKS_HAVE_IDLED); - } - - if (!idlingResourceRegistry.allResourcesAreIdle()) { - final IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy(); - final IdlingPolicy error = IdlingPolicies.getDynamicIdlingResourceErrorPolicy(); - final SignalingTask<Void> idleSignal = new SignalingTask<Void>(NO_OP, - IdleCondition.DYNAMIC_TASKS_HAVE_IDLED, generation); - idlingResourceRegistry.notifyWhenAllResourcesAreIdle(new IdleNotificationCallback() { - @Override - public void resourcesStillBusyWarning(List<String> busyResourceNames) { - warning.handleTimeout(busyResourceNames, "IdlingResources are still busy!"); - } - - @Override - public void resourcesHaveTimedOut(List<String> busyResourceNames) { - error.handleTimeout(busyResourceNames, "IdlingResources have timed out!"); - controllerHandler.post(idleSignal); - } - - @Override - public void allResourcesIdle() { - controllerHandler.post(idleSignal); - } - }); - condChecks.add(IdleCondition.DYNAMIC_TASKS_HAVE_IDLED); - } - - try { - loopUntil(condChecks); - } finally { - asyncTaskMonitor.cancelIdleMonitor(); - if (compatTaskMonitor.isPresent()) { - compatTaskMonitor.get().cancelIdleMonitor(); - } - idlingResourceRegistry.cancelIdleMonitor(); - } - } while (!asyncTaskMonitor.isIdleNow() || !compatIdle() - || !idlingResourceRegistry.allResourcesAreIdle()); - - } - - private boolean compatIdle() { - if (compatTaskMonitor.isPresent()) { - return compatTaskMonitor.get().isIdleNow(); - } else { - return true; - } - } - - @Override - public void loopMainThreadForAtLeast(long millisDelay) { - initialize(); - checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!"); - checkState(!IdleCondition.DELAY_HAS_PAST.isSignaled(conditionSet), "recursion detected!"); - - checkArgument(millisDelay > 0); - controllerHandler.postDelayed(new SignalingTask(NO_OP, IdleCondition.DELAY_HAS_PAST, - generation), - millisDelay); - loopUntil(IdleCondition.DELAY_HAS_PAST); - loopMainThreadUntilIdle(); - } - - @Override - public boolean handleMessage(Message msg) { - if (!IdleCondition.handleMessage(msg, conditionSet, generation)) { - Log.i(TAG, "Unknown message type: " + msg); - return false; - } else { - return true; - } - } - - private void loopUntil(IdleCondition condition) { - loopUntil(EnumSet.of(condition)); - } - - /** - * Loops the main thread until all IdleConditions have been signaled. - * - * Once they've been signaled, the conditions are reset and the generation value - * is incremented. - * - * Signals should only be raised thru SignalingTask instances, and care should be - * taken to ensure that the signaling task is created before loopUntil is called. - * - * Good: - * idlingType.runOnIdle(new SignalingTask(NO_OP, IdleCondition.MY_IDLE_CONDITION, generation)); - * loopUntil(IdleCondition.MY_IDLE_CONDITION); - * - * Bad: - * idlingType.runOnIdle(new CustomCallback() { - * @Override - * public void itsDone() { - * // oh no - The creation of this signaling task is delayed until this method is - * // called, so it will not have the right value for generation. - * new SignalingTask(NO_OP, IdleCondition.MY_IDLE_CONDITION, generation).run(); - * } - * }) - * loopUntil(IdleCondition.MY_IDLE_CONDITION); - */ - private void loopUntil(EnumSet<IdleCondition> conditions) { - checkState(!looping, "Recursive looping detected!"); - looping = true; - IdlingPolicy masterIdlePolicy = IdlingPolicies.getMasterIdlingPolicy(); - try { - int loopCount = 0; - long start = SystemClock.uptimeMillis(); - long end = start + masterIdlePolicy.getIdleTimeoutUnit().toMillis( - masterIdlePolicy.getIdleTimeout()); - while (SystemClock.uptimeMillis() < end) { - boolean conditionsMet = true; - boolean shouldLogConditionState = loopCount > 0 && loopCount % 100 == 0; - - for (IdleCondition condition : conditions) { - if (!condition.isSignaled(conditionSet)) { - conditionsMet = false; - if (shouldLogConditionState) { - Log.w(TAG, "Waiting for: " + condition.name() + " for " + loopCount + " iterations."); - } else { - break; - } - } - } - - if (conditionsMet) { - QueueState queueState = queueInterrogator.determineQueueState(); - if (queueState == QueueState.EMPTY || queueState == QueueState.TASK_DUE_LONG) { - return; - } else { - Log.v( - "ESP_TRACE", - - "Barrier detected or task avaliable for running shortly."); - } - } - - Message message = queueInterrogator.getNextMessage(); - String callbackString = "unknown"; - String messageString = "unknown"; - try { - if (null == message.getCallback()) { - callbackString = "no callback."; - } else { - callbackString = message.getCallback().toString(); - } - messageString = message.toString(); - } catch (NullPointerException e) { - /* - * Ignore. android.app.ActivityThread$ActivityClientRecord#toString() fails for API level - * 15. - */ - } - - Log.v( - "ESP_TRACE", - String.format("%s: MessageQueue.next(): %s, with target: %s, callback: %s", TAG, - messageString, message.getTarget().getClass().getCanonicalName(), callbackString)); - message.getTarget().dispatchMessage(message); - message.recycle(); - loopCount++; - } - List<String> idleConditions = Lists.newArrayList(); - for (IdleCondition condition : conditions) { - if (!condition.isSignaled(conditionSet)) { - idleConditions.add(condition.name()); - } - } - masterIdlePolicy.handleTimeout(idleConditions, String.format( - "Looped for %s iterations over %s %s.", loopCount, masterIdlePolicy.getIdleTimeout(), - masterIdlePolicy.getIdleTimeoutUnit().name())); - } finally { - looping = false; - generation++; - for (IdleCondition condition : conditions) { - condition.reset(conditionSet); - } - } - } - - - private void initialize() { - if (controllerHandler == null) { - controllerHandler = new Handler(this); - } - } - - - /** - * Encapsulates posting a signal message to update the conditions set after a task has - * executed. - */ - private class SignalingTask<T> extends FutureTask<T> { - - private final IdleCondition condition; - private final int myGeneration; - - public SignalingTask(Callable<T> callable, IdleCondition condition, int myGeneration) { - super(callable); - this.condition = checkNotNull(condition); - this.myGeneration = myGeneration; - } - - @Override - protected void done() { - controllerHandler.sendMessage(condition.createSignal(controllerHandler, myGeneration)); - } - - } - -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImpl.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImpl.java deleted file mode 100644 index 30e0658..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImpl.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.android.apps.common.testing.ui.espresso.util.TreeIterables.breadthFirstViewTraversal; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import com.google.android.apps.common.testing.ui.espresso.AmbiguousViewMatcherException; -import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException; -import com.google.android.apps.common.testing.ui.espresso.ViewFinder; -import com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers; -import com.google.common.base.Joiner; -import com.google.common.base.Optional; -import com.google.common.base.Predicate; -import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; -import com.google.common.collect.Lists; - -import android.os.Looper; -import android.view.View; -import android.widget.AdapterView; - -import org.hamcrest.Matcher; - -import java.util.Iterator; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Provider; - -/** - * Implementation of {@link ViewFinder}. - */ -// TODO(user): in the future we may want to collect stats here about the size of the view -// hierarchy, average matcher execution time, warn when matchers take too long to execute, etc. -public final class ViewFinderImpl implements ViewFinder { - - private final Matcher<View> viewMatcher; - private final Provider<View> rootViewProvider; - - @Inject - ViewFinderImpl(Matcher<View> viewMatcher, Provider<View> rootViewProvider) { - this.viewMatcher = viewMatcher; - this.rootViewProvider = rootViewProvider; - } - - @Override - public View getView() throws AmbiguousViewMatcherException, NoMatchingViewException { - checkMainThread(); - final Predicate<View> matcherPredicate = new MatcherPredicateAdapter<View>( - checkNotNull(viewMatcher)); - - View root = rootViewProvider.get(); - Iterator<View> matchedViewIterator = Iterables.filter( - breadthFirstViewTraversal(root), - matcherPredicate).iterator(); - - View matchedView = null; - - while (matchedViewIterator.hasNext()) { - if (matchedView != null) { - // Ambiguous! - throw new AmbiguousViewMatcherException.Builder() - .withViewMatcher(viewMatcher) - .withRootView(root) - .withView1(matchedView) - .withView2(matchedViewIterator.next()) - .withOtherAmbiguousViews(Iterators.toArray(matchedViewIterator, View.class)) - .build(); - } else { - matchedView = matchedViewIterator.next(); - } - } - if (null == matchedView) { - final Predicate<View> adapterViewPredicate = new MatcherPredicateAdapter<View>( - ViewMatchers.isAssignableFrom(AdapterView.class)); - List<View> adapterViews = Lists.newArrayList( - Iterables.filter(breadthFirstViewTraversal(root), adapterViewPredicate).iterator()); - if (adapterViews.isEmpty()) { - throw new NoMatchingViewException.Builder() - .withViewMatcher(viewMatcher) - .withRootView(root) - .build(); - } - - String warning = String.format("\nIf the target view is not part of the view hierarchy, you " - + "may need to use Espresso.onData to load it from one of the following AdapterViews:%s" - , Joiner.on("\n- ").join(adapterViews)); - throw new NoMatchingViewException.Builder() - .withViewMatcher(viewMatcher) - .withRootView(root) - .withAdapterViews(adapterViews) - .withAdapterViewWarning(Optional.of(warning)) - .build(); - } else { - return matchedView; - } - } - - private void checkMainThread() { - checkState(Thread.currentThread().equals(Looper.getMainLooper().getThread()), - "Executing a query on the view hierarchy outside of the main thread (on: %s)", - Thread.currentThread().getName()); - } - - private static class MatcherPredicateAdapter<T> implements Predicate<T> { - private final Matcher<? super T> matcher; - - private MatcherPredicateAdapter(Matcher<? super T> matcher) { - this.matcher = checkNotNull(matcher); - } - - @Override - public boolean apply(T input) { - return matcher.matches(input); - } - } -} diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/WindowManagerEventInjectionStrategy.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/WindowManagerEventInjectionStrategy.java deleted file mode 100644 index 05792e7..0000000 --- a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/WindowManagerEventInjectionStrategy.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * 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.apps.common.testing.ui.espresso.base; - -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Throwables.propagate; - -import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException; - -import android.os.Build; -import android.os.IBinder; -import android.util.Log; -import android.view.KeyEvent; -import android.view.MotionEvent; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * An {@link EventInjectionStrategy} that uses the window manager to inject {@link MotionEvent}s. - * This strategy supports API level 15 and below. - */ -final class WindowManagerEventInjectionStrategy implements EventInjectionStrategy { - private static final String TAG = WindowManagerEventInjectionStrategy.class.getSimpleName(); - - - WindowManagerEventInjectionStrategy() { - checkState(Build.VERSION.SDK_INT >= 7 && Build.VERSION.SDK_INT <= 15, "Unsupported API level."); - } - - // Reflection members. - private boolean initComplete; - private Object wmInstance; - private Method injectInputKeyEventMethod; - private Method injectInputMotionEventMethod; - - void initialize() { - if (initComplete) { - return; - } - - try { - Log.d(TAG, "Trying to create injection strategy."); - - Class<?> serviceManagerClassObj = Class.forName("android.os.ServiceManager"); - Method windowServiceMethod = - serviceManagerClassObj.getDeclaredMethod("getService", String.class); - windowServiceMethod.setAccessible(true); - - Object windowServiceBinderObj = windowServiceMethod.invoke(serviceManagerClassObj, "window"); - - Class<?> windowManagerStubObject = Class.forName("android.view.IWindowManager$Stub"); - Method asInterfaceMethod = - windowManagerStubObject.getDeclaredMethod("asInterface", IBinder.class); - asInterfaceMethod.setAccessible(true); - - wmInstance = asInterfaceMethod.invoke(windowManagerStubObject, windowServiceBinderObj); - - injectInputMotionEventMethod = wmInstance.getClass() - .getDeclaredMethod("injectPointerEvent", MotionEvent.class, Boolean.TYPE); - injectInputMotionEventMethod.setAccessible(true); - - injectInputKeyEventMethod = - wmInstance.getClass().getDeclaredMethod("injectKeyEvent", KeyEvent.class, Boolean.TYPE); - injectInputMotionEventMethod.setAccessible(true); - - initComplete = true; - } catch (ClassNotFoundException e) { - propagate(e); - } catch (IllegalAccessException e) { - propagate(e); - } catch (IllegalArgumentException e) { - propagate(e); - } catch (InvocationTargetException e) { - propagate(e); - } catch (NoSuchMethodException e) { - propagate(e); - } catch (SecurityException e) { - propagate(e); - } - } - - @Override - public boolean injectKeyEvent(KeyEvent keyEvent) throws InjectEventSecurityException { - try { - // From javadoc of com.android.server.WindowManagerService.injectKeyEvent: - // @param sync If true, wait for the event to be completed before returning to the caller. - // @return true if event was dispatched, false if it was dropped for any reason - // - // Key events are delivered OFF the main thread, and we block until they are processed. - return (Boolean) injectInputKeyEventMethod.invoke(wmInstance, keyEvent, true); - } catch (IllegalAccessException e) { - propagate(e); - } catch (IllegalArgumentException e) { - propagate(e); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - if (cause instanceof SecurityException) { - throw new InjectEventSecurityException(cause); - } - propagate(e); - } catch (SecurityException e) { - throw new InjectEventSecurityException(e); - } - return false; - } - - @Override - public boolean injectMotionEvent(MotionEvent motionEvent) throws InjectEventSecurityException { - try { - // From javadoc of com.android.server.WindowManagerService.injectKeyEvent: - // @param sync If true, wait for the event to be completed before returning to the caller. - // @return true if event was dispatched, false if it was dropped for any reason - // - // We inject the pointer with sync=true to ensure the event is dispatched before control - // is returned to our code. - return (Boolean) injectInputMotionEventMethod.invoke( - wmInstance, - motionEvent, - true /* sync */); - } catch (IllegalAccessException e) { - propagate(e); - } catch (IllegalArgumentException e) { - propagate(e); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - if (cause instanceof SecurityException) { - throw new InjectEventSecurityException(cause); - } - propagate(e); - } catch (SecurityException e) { - throw new InjectEventSecurityException(e); - } - return false; - } -} |