InstrumentationActivityInvoker.java
/*
* Copyright (C) 2018 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 androidx.test.core.app;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static androidx.test.InstrumentationRegistry.getTargetContext;
import static androidx.test.internal.util.Checks.checkState;
import android.app.Activity;
import android.content.Intent;
import androidx.test.internal.platform.app.ActivityInvoker;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
/**
* On-device {@link ActivityInvoker} implementation that drives Activity lifecycles using {@link
* android.app.ActivityManager} indirectly via {@link Activity#startActivity} and {@link
* Activity#recreate}.
*
* <p>All the methods in this class are non-blocking API.
*/
class InstrumentationActivityInvoker implements ActivityInvoker {
/**
* An empty activity with style "android:windowIsFloating = false". The style is set by
* AndroidManifest.xml via "android:theme".
*
* <p>This activity is used to send an arbitrary resumed Activity to stopped.
*/
public static class EmptyActivity extends Activity {};
/**
* An empty activity with style "android:windowIsFloating = true". The style is set by
* AndroidManifest.xml via "android:theme".
*
* <p>This activity is used to send an arbitrary resumed Activity to paused.
*/
public static class EmptyFloatingActivity extends Activity {};
/**
* Starts an Activity using the given intent. FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_CLEAR_TOP
* flags are set to the intent to start the Activity in brand-new task stack. Note: {@link
* Instrumentation#startActivitySync} cannot be used here because it has an assertion that the
* resolved Activity's process name equals to the test target package name. This is to ensure that
* the Activity is launched in the same process as instrumentation but it is possible to run
* Activities with different process name in a same process if they share the same
* android:sharedUserId in the both AndroidManifests.
*/
@Override
public void startActivity(Intent intent) {
getTargetContext()
.startActivity(
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP));
}
/**
* Resumes the Activity by issuing a start activity intent with {@link
* Intent#FLAG_ACTIVITY_REORDER_TO_FRONT} flag, that brings back the Activity to the top of the
* history stack (or starts new one if the Activity is not found in the stack).
*/
@Override
public void resumeActivity(Activity activity) {
getInstrumentation()
.runOnMainSync(
() -> {
Stage stage =
ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
checkState(
stage == Stage.RESUMED || stage == Stage.PAUSED || stage == Stage.STOPPED,
"Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.",
stage);
activity.startActivity(
activity.getIntent().setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
});
}
/**
* Pauses the Activity by issuing a start activity intent of {@link EmptyFloatingActivity} with
* {@link Intent#FLAG_ACTIVITY_REORDER_TO_FRONT} flag, that brings back the Activity to the top of
* the history stack (or starts new one if the Activity is not found in the stack).
*/
@Override
public void pauseActivity(Activity activity) {
getInstrumentation()
.runOnMainSync(
() -> {
Stage stage =
ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
checkState(
stage == Stage.RESUMED || stage == Stage.PAUSED,
"Activity's stage must be RESUMED or PAUSED but was %s.",
stage);
// Starting an arbitrary Activity (android:windowIsFloating = true) forces the tested
// Activity
// to the paused and still visible state.
activity.startActivity(
getIntentForActivity(EmptyFloatingActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
});
}
/**
* Stops the Activity by issuing a start activity intent of {@link EmptyActivity} with {@link
* Intent#FLAG_ACTIVITY_REORDER_TO_FRONT} flag, that brings back the Activity to the top of the
* history stack (or starts new one if the Activity is not found in the stack).
*/
@Override
public void stopActivity(Activity activity) {
getInstrumentation()
.runOnMainSync(
() -> {
Stage stage =
ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
checkState(
stage == Stage.RESUMED || stage == Stage.PAUSED || stage == Stage.STOPPED,
"Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.",
stage);
// Starting an arbitrary Activity (android:windowIsFloating = false) forces the tested
// Activity to the stopped state.
activity.startActivity(
getIntentForActivity(EmptyActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
});
}
/**
* Recreates the Activity by {@link Activity#recreate}.
*
* <p>Note that {@link Activity#recreate}'s behavior differs by Android framework version. For
* example, the version P brings Activity's lifecycle state to the original state after the
* re-creation. A stopped Activity goes to stopped state after the re-creation in concrete.
* Whereas the version O ignores {@link Activity#recreate} method call when the activity is in
* stopped state. The version N re-creates stopped Activity but brings back to paused state
* instead of stopped.
*
* <p>In short, make sure to set Activity's state to resumed before calling this method otherwise
* the behavior is the framework version dependent.
*/
@Override
public void recreateActivity(final Activity activity) {
getInstrumentation()
.runOnMainSync(
() -> {
Stage stage =
ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity);
checkState(
stage == Stage.RESUMED || stage == Stage.PAUSED || stage == Stage.STOPPED,
"Activity's stage must be RESUMED, PAUSED or STOPPED but was %s.",
stage);
activity.recreate();
});
}
}