Programatically issuing a long-press back key from Espresso

  android, android-espresso

My Espresso integration test needs to issue a long-press Back button to the app under test, without requiring any changes to app source code. While my tests can successfully issue a normal Back button (via androidx.test.espresso.Espresso.pressBack()) and a long-press to a given pixel location on the display (via androidx.test.espresso.action.GeneralClickAction), they cannot successfully issue a long-press Back button. The app shows no change to the UI, though it works just fine if manually long-pressed during test operation.

How do I get it to work? Borrowing from Espresso, I tried the following, changing only the class names and KeyEvent constructors.

onView(isRoot()).perform(ViewActions.actionWithAssertions(new PressLongBackAction(true)));

***public final class PressLongBackAction extends LongPressKeyEventActionBase {

private final boolean conditional;

public PressLongBackAction(boolean conditional) {
    this(conditional, new EspressoKey.Builder().withKeyCode(KeyEvent.KEYCODE_BACK).build());
}

public PressLongBackAction(boolean conditional, EspressoKey espressoKey) {
    super(espressoKey);
    this.conditional = conditional;
}

@Override
public void perform(UiController uiController, View view) {

    Activity initialActivity = getCurrentActivity();

    new Exception().printStackTrace(System.out);
    super.perform(uiController, view);
    new Exception().printStackTrace(System.out);

    // Wait for a Stage change of the initial activity.
    waitForStageChangeInitialActivity(uiController, initialActivity);
    // Wait until there are no other pending activities in a foreground stage.
    waitForPendingForegroundActivities(uiController, conditional);
}

}


***class LongPressKeyEventActionBase implements ViewAction {
private static final String TAG = "KeyEventTestActionBase";

public static final int BACK_ACTIVITY_TRANSITION_MILLIS_DELAY = 150;
public static final int CLEAR_TRANSITIONING_ACTIVITIES_ATTEMPTS = 4;
public static final int CLEAR_TRANSITIONING_ACTIVITIES_MILLIS_DELAY = 150;

final EspressoKey espressoKey;

LongPressKeyEventActionBase(EspressoKey espressoKey) {
    this.espressoKey = checkNotNull(espressoKey);
}

@Override
public Matcher<View> getConstraints() {
    return isDisplayed();
}

@Override
public String getDescription() {
    return String.format(Locale.ROOT, "send %s key event", this.espressoKey);
}

@Override
public void perform(UiController uiController, View view) {
    try {
        if (!sendKeyEvent(uiController)) {
            Log.e(TAG, "Failed to inject espressoKey event: " + this.espressoKey);
            throw new PerformException.Builder()
                    .withActionDescription(this.getDescription())
                    .withViewDescription(HumanReadables.describe(view))
                    .withCause(
                            new RuntimeException("Failed to inject espressoKey event " + this.espressoKey))
                    .build();
        }
    } catch (InjectEventSecurityException e) {
        Log.e(TAG, "Failed to inject espressoKey event: " + this.espressoKey);
        throw new PerformException.Builder()
                .withActionDescription(this.getDescription())
                .withViewDescription(HumanReadables.describe(view))
                .withCause(e)
                .build();
    }
}

private boolean sendKeyEvent(UiController controller) throws InjectEventSecurityException {

    boolean injected = false;
    long eventTime = SystemClock.uptimeMillis();
    for (int attempts = 0; !injected && attempts < 4; attempts++) {
        final KeyEvent keyEvent = new KeyEvent(
                eventTime,
                eventTime,
                KeyEvent.ACTION_DOWN,
                this.espressoKey.getKeyCode(),
                0,
                this.espressoKey.getMetaState(),
                -1,
                0,
                KeyEvent.FLAG_LONG_PRESS);
        Log.d(TAG, "keyEvent : " + keyEvent.toString());
        injected = controller.injectKeyEvent(keyEvent);
    }

    if (!injected) {
        // it is not a transient failure... :(
        return false;
    }

    injected = false;
    eventTime = SystemClock.uptimeMillis();
    for (int attempts = 0; !injected && attempts < 4; attempts++) {
        final KeyEvent keyEvent = new KeyEvent(
                eventTime,
                eventTime,
                KeyEvent.ACTION_UP,
                this.espressoKey.getKeyCode(),
                0,
                this.espressoKey.getMetaState(),
                -1,
                0,
                KeyEvent.FLAG_LONG_PRESS );
        Log.d(TAG, "keyEvent : " + keyEvent.toString());
        injected = controller.injectKeyEvent(keyEvent);
    }

    return injected;
}

static Activity getCurrentActivity() {
    Collection<Activity> resumedActivities =
            ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
    return getOnlyElement(resumedActivities);
}

static void waitForStageChangeInitialActivity(UiController controller, Activity initialActivity) {
    if (isActivityResumed(initialActivity)) {
        // The activity transition hasn't happened yet, wait for it.
        controller.loopMainThreadForAtLeast(BACK_ACTIVITY_TRANSITION_MILLIS_DELAY);
        if (isActivityResumed(initialActivity)) {
            Log.e(
                    TAG,
                    "Back was pressed but there was no Activity stage transition in "
                            + BACK_ACTIVITY_TRANSITION_MILLIS_DELAY
                            + "ms, possibly due to a delay calling super.onBackPressed() from your Activity.");
        }
    }
}

private static boolean isActivityResumed(Activity activity) {
    return ActivityLifecycleMonitorRegistry.getInstance().getLifecycleStageOf(activity)
            == Stage.RESUMED;
}

static void waitForPendingForegroundActivities(UiController controller, boolean conditional) {
    ActivityLifecycleMonitor activityLifecycleMonitor =
            ActivityLifecycleMonitorRegistry.getInstance();
    boolean pendingForegroundActivities = false;
    for (int attempts = 0; attempts < CLEAR_TRANSITIONING_ACTIVITIES_ATTEMPTS; attempts++) {
        controller.loopMainThreadUntilIdle();
        pendingForegroundActivities = hasTransitioningActivities(activityLifecycleMonitor);
        if (pendingForegroundActivities) {
            controller.loopMainThreadForAtLeast(CLEAR_TRANSITIONING_ACTIVITIES_MILLIS_DELAY);
        } else {
            break;
        }
    }

    // Pressing back can kill the app: log a warning.
    if (!hasForegroundActivities(activityLifecycleMonitor)) {
        if (conditional) {
            throw new NoActivityResumedException("Pressed back and killed the app");
        }
        Log.w(TAG, "Pressed back and hopped to a different process or potentially killed the app");
    }

    if (pendingForegroundActivities) {
        Log.e(
                TAG,
                "Back was pressed and left the application in an inconsistent state even after "
                        + (CLEAR_TRANSITIONING_ACTIVITIES_MILLIS_DELAY
                        * CLEAR_TRANSITIONING_ACTIVITIES_ATTEMPTS)
                        + "ms.");
    }
}

}


The KeyEvent for a normal press on the Back button looks like this: ***logcat:

2021-05-12 11:49:57.501 31118-31118/com… D/…: keyEvent : KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x80, repeatCount=0, eventTime=1234519744, downTime=1234519744, deviceId=-1, displayId=0, source=0x0 }

2021-05-12 11:49:57.508 31118-31118/com… D/[email protected][ActivityMain]: ViewPostImeInputStage processKey 0

2021-05-12 11:49:57.510 31118-31118/com… D/…: keyEvent : KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x80, repeatCount=0, eventTime=1234519753, downTime=1234519753, deviceId=-1, displayId=0, source=0x0 }

2021-05-12 11:49:57.513 31118-31118/com… D/[email protected][ActivityMain]: ViewPostImeInputStage processKey 1

2021-05-12 11:50:04.682 31118-31118/com… D/…: keyEvent : KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x0, repeatCount=0, eventTime=1234526925, downTime=1234526925, deviceId=-1, displayId=0, source=0x0 }

2021-05-12 11:50:04.686 31118-31118/com… D/[email protected][ActivityMain]: ViewPostImeInputStage processKey 0

2021-05-12 11:50:04.688 31118-31118/com… D/…: keyEvent : KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x0, repeatCount=0, eventTime=1234526931, downTime=1234526931, deviceId=-1, displayId=0, source=0x0 }

2021-05-12 11:50:04.691 31118-31118/com… D/[email protected][ActivityMain]: ViewPostImeInputStage processKey 1

2021-05-12 11:50:05.101 1314-1418/? V/WindowManager: Relayout Window{5dfbe22d0 u0 com…/com…ui.ActivityMain}: viewVisibility=0 req=2048×1536 WM.LayoutParams{(0,0)(fillxfill) sim=#20 ty=1 fl=#410500 wanim=0x1030465 needsMenuKey=1 naviIconColor=0}

2021-05-12 11:50:05.107 31118-31118/com… D/[email protected][ActivityMain]: Relayout returned: oldFrame=[0,0][2048,1536] newFrame=[0,0][2048,1536] result=0x1 surface={isValid=true 547395848192} surfaceGenerationChanged=false


Source: Android Questions

LEAVE A COMMENT