blob: 7dcb05b4172494b1020dad7ad3841a67b0a8fd0d [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package androidx.test.espresso.flutter.action;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import android.os.Looper;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.UiController;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
/** Utils for the Flutter actions. */
final class ActionUtil {
/**
* Loops the main thread until the given future task has been done. Users could use this method to
* "synchronize" between the main thread and {@code Future} instances running on its own thread
* (e.g. methods of the {@code FlutterTestingProtocol}), without blocking the main thread.
*
* <p>Usage:
*
* <pre>{@code
* Future<T> fooFuture = flutterTestingProtocol.callFoo();
* T fooResult = loopUntilCompletion("fooTask", androidUiController, fooFuture, executor);
* // Then consumes the fooResult on main thread.
* }</pre>
*
* @param taskName the name that shall be used when registering the task as an {@link
* IdlingResource}. Espresso ignores {@link IdlingResource} with the same name, so always uses
* a unique name if you don't want Espresso to ignore your task.
* @param androidUiController the controller to use to interact with the Android UI.
* @param futureTask the future task that main thread should wait for a completion signal.
* @param executor the executor to use for running async tasks within the method.
* @param <T> the return value type.
* @return the result of the future task.
* @throws ExecutionException if any error occurs during executing the future task.
* @throws InterruptedException when any internal thread is interrupted.
*/
public static <T> T loopUntilCompletion(
String taskName,
UiController androidUiController,
Future<T> futureTask,
ExecutorService executor)
throws ExecutionException, InterruptedException {
checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
FutureIdlingResource<T> idlingResourceFuture = new FutureIdlingResource<>(taskName, futureTask);
IdlingRegistry.getInstance().register(idlingResourceFuture);
try {
// It's fine to ignore this {@code Future} handler, since {@code idlingResourceFuture} should
// give us the result/error any way.
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError = executor.submit(idlingResourceFuture);
androidUiController.loopMainThreadUntilIdle();
checkState(idlingResourceFuture.isDone(), "Future task signaled - but it wasn't done.");
return idlingResourceFuture.get();
} finally {
IdlingRegistry.getInstance().unregister(idlingResourceFuture);
}
}
/**
* An {@code IdlingResource} implementation that takes in a {@code Future}, and sends the idle
* signal to the main thread when the given {@code Future} is done.
*
* @param <T> the return value type of this {@code FutureTask}.
*/
private static class FutureIdlingResource<T> extends FutureTask<T> implements IdlingResource {
private final String taskName;
// Written from main thread, read from any thread.
private volatile ResourceCallback resourceCallback;
public FutureIdlingResource(String taskName, final Future<T> future) {
super(
new Callable<T>() {
@Override
public T call() throws Exception {
return future.get();
}
});
this.taskName = checkNotNull(taskName);
}
@Override
public String getName() {
return taskName;
}
@Override
public void done() {
resourceCallback.onTransitionToIdle();
}
@Override
public boolean isIdleNow() {
return isDone();
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
this.resourceCallback = callback;
}
}
}