blob: 3cfbb98bed672884d3b27fac9ccdd52b117349e7 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.test.util;
import android.app.Activity;
import androidx.test.runner.lifecycle.ActivityLifecycleCallback;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitor;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import org.junit.Assert;
import org.chromium.base.ThreadUtils;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/** Methods used for testing Application-level behavior. */
public class ApplicationTestUtils {
private static final ActivityLifecycleMonitor sMonitor =
ActivityLifecycleMonitorRegistry.getInstance();
private static final long ACTIVITY_TIMEOUT = 10000;
/** Waits until the given activity transitions to the given state. */
public static void waitForActivityState(Activity activity, Stage stage) {
waitForActivityState(
"Activity "
+ activity.getLocalClassName()
+ " did not reach stage: "
+ stage
+ ". Is the device screen turned on?",
activity,
stage);
}
/** Waits until the given activity transitions to the given state. */
public static void waitForActivityState(String failureReason, Activity activity, Stage stage) {
CriteriaHelper.pollUiThread(
() -> {
return sMonitor.getLifecycleStageOf(activity) == stage;
},
failureReason,
ACTIVITY_TIMEOUT,
CriteriaHelper.DEFAULT_POLLING_INTERVAL);
// De-flake by flushing the tasks that are already queued on the Looper's Handler.
// TODO(https://crbug.com/1424788): Remove this and properly fix flaky tests.
TestThreadUtils.flushNonDelayedLooperTasks();
}
/** Finishes the given activity and waits for its onDestroy() to be called. */
public static void finishActivity(final Activity activity) throws Exception {
ThreadUtils.runOnUiThreadBlocking(
() -> {
if (sMonitor.getLifecycleStageOf(activity) != Stage.DESTROYED) {
activity.finish();
}
});
final String error =
"Failed to finish the Activity. Did you start a second Activity and "
+ "not finish it?";
waitForActivityState(error, activity, Stage.DESTROYED);
}
/**
* Recreates the provided Activity, returning the newly created Activity once it's finished
* starting up.
* @param activity The Activity to recreate.
* @return The newly created Activity.
*/
public static <T extends Activity> T recreateActivity(T activity) {
return waitForActivityWithClass(
activity.getClass(), Stage.RESUMED, () -> activity.recreate());
}
/**
* Waits for an activity of the specified class to reach the specified Activity {@link Stage},
* triggered by running the provided trigger.
*
* @param activityClass The class type to wait for.
* @param state The Activity {@link Stage} to wait for an activity of the right class type to
* reach.
* @param uiThreadTrigger The Runnable that will trigger the state change to wait for. The
* Runnable will be run on the UI thread
*/
public static <T extends Activity> T waitForActivityWithClass(
Class<? extends Activity> activityClass, Stage stage, Runnable uiThreadTrigger) {
return waitForActivityWithClass(activityClass, stage, uiThreadTrigger, null);
}
/**
* Waits for an activity of the specified class to reach the specified Activity {@link Stage},
* triggered by running the provided trigger.
*
* @param activityClass The class type to wait for.
* @param state The Activity {@link Stage} to wait for an activity of the right class type to
* reach.
* @param uiThreadTrigger The Runnable that will trigger the state change to wait for, which
* will be run on the UI thread.
* @param backgroundThreadTrigger The Runnable that will trigger the state change to wait for,
* which will be run on the UI thread.
*/
public static <T extends Activity> T waitForActivityWithClass(
Class<? extends Activity> activityClass,
Stage stage,
Runnable uiThreadTrigger,
Runnable backgroundThreadTrigger) {
ThreadUtils.assertOnBackgroundThread();
final CallbackHelper activityCallback = new CallbackHelper();
final AtomicReference<T> activityRef = new AtomicReference<>();
ActivityLifecycleCallback stateListener =
(Activity newActivity, Stage newStage) -> {
if (newStage == stage) {
if (!activityClass.isAssignableFrom(newActivity.getClass())) return;
activityRef.set((T) newActivity);
ThreadUtils.postOnUiThread(() -> activityCallback.notifyCalled());
}
};
sMonitor.addLifecycleCallback(stateListener);
try {
if (uiThreadTrigger != null) {
ThreadUtils.runOnUiThreadBlocking(() -> uiThreadTrigger.run());
}
if (backgroundThreadTrigger != null) backgroundThreadTrigger.run();
activityCallback.waitForFirst(
"No Activity reached target state.", ACTIVITY_TIMEOUT, TimeUnit.MILLISECONDS);
T createdActivity = activityRef.get();
Assert.assertNotNull("Activity reference is null.", createdActivity);
return createdActivity;
} catch (TimeoutException e) {
throw new RuntimeException(e);
} finally {
sMonitor.removeLifecycleCallback(stateListener);
}
}
}