Skip to content

fix(android): handle each back action once - #2029

Open
GitToTheHub wants to merge 1 commit into
apache:masterfrom
GitToTheHub:fix/android-backbutton-once-2028
Open

GitToTheHub wants to merge 1 commit into
apache:masterfrom
GitToTheHub:fix/android-backbutton-once-2028

Conversation

@GitToTheHub

Copy link
Copy Markdown
Contributor

Platforms affected

Android

Motivation and Context

On Android 16, one Back key press can invoke the AndroidX back callback and then deliver the original key to the WebView. Cordova's synthetic key redispatch therefore fires backbutton twice, causing applications to navigate back two screens. Edge swipes fire once.

Fixes #2028.

Description

Replace synthetic DOWN/UP keys with a shared back-action handler. Callbacks and legacy key releases use the same action order: dismiss a custom view, notify a JavaScript backbutton listener, or navigate WebView history. Unhandled callback actions delegate to Android with the callback temporarily disabled to avoid recursion.

Keep a single callback timestamp in the WebView to consume an original key already handled by the callback. Clear it when the JavaScript override is removed. This avoids a debounce interval and preserves rapid consecutive inputs.

Testing

  • Six instrumentation regressions pass on Android 15 (API 35, WebView 149) and Android 16 (API 36, WebView 150): callback plus original key, consecutive callbacks, later legacy key, custom-view dismissal, history navigation, and unhandled Back.
  • A sample app on both versions produces one event per real Back key press and edge swipe, including consecutive key presses about 21–23 ms apart.
  • npm run lint, all 335 npm run unit-tests specs, and git diff --check pass.

The existing instrumentation project does not build unmodified: Kotlin stdlib versions conflict, androidx.test:rules is missing, and BackButtonMultipageTest has an unhandled InterruptedException. For the targeted run, a temporary Gradle init script aligned Kotlin jdk7/jdk8 to 1.8.22, added androidx.test:rules:1.1.1, and limited instrumentation compilation to BackButtonDispatchTest. The APK build excluded the unrelated :CordovaLib:extractDebugAnnotations task. Those infrastructure workarounds are not part of this PR. The full instrumentation suite and Android 14 or older were not verified.

Checklist

  • I've run the tests to see all new and existing tests pass (targeted checks pass; full instrumentation suite has the existing issues described above)
  • I added automated test coverage as appropriate for this change
  • Commit is prefixed with (platform) if this change only applies to one platform
  • If this Pull Request resolves an issue, I linked to the issue in the text above and used a closing keyword
  • I've updated the documentation if necessary (JavaDoc for the shared handler)

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.20%. Comparing base (2b1248f) to head (9493127).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #2029   +/-   ##
=======================================
  Coverage   63.20%   63.20%           
=======================================
  Files          24       24           
  Lines        4922     4922           
=======================================
  Hits         3111     3111           
  Misses       1811     1811           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Intent intent = new Intent();
intent.putExtra("startUrl", "https://localhost/backbuttondispatch/index.html");
activity = activityRule.launchActivity(intent);
webView = (WebView) activity.getWebInterface().getView();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test retrieves the existing Cordova WebView; it does not enable content access or change any WebView security settings. The finding exposes the existing WebView configuration through the new test. Any production change to content:// access should be considered in a separate PR with compatibility coverage, so this back-button fix remains focused.


private boolean performBackButtonAction() {
if (mCustomView != null) {
hideCustomView();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call already existed in the Back key handler and has been moved into the shared back-action helper. Preserving custom-view dismissal is necessary so closing a full-screen view does not also fire backbutton and navigate the app. Modernizing the deprecated custom-view API should be handled separately; this PR preserves the existing behavior.

Comment on lines +114 to +115
activity.getWebInterface().showCustomView(new View(activity),
() -> customViewHidden.set(true));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regression test deliberately exercises the existing showCustomView API to verify that a Back action dismisses the custom view once without also firing a JavaScript backbutton event. The API is deprecated, but the behavior is still supported and needs coverage. Custom-view API modernization can be handled in a separate PR.

private String evaluate(String script) throws Throwable {
ArrayBlockingQueue<String> result = new ArrayBlockingQueue<>(1);
activityRule.runOnUiThread(() -> webView.evaluateJavascript(script, result::add));
String value = result.poll(10, TimeUnit.SECONDS);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a false positive. value is read immediately after this assignment by assertNotNull("JavaScript evaluation timed out", value) and then returned with return value;. Keeping the local variable allows the test to report a clear timeout failure before returning the JavaScript result.

@2zovd

2zovd commented Oct 13, 2026 •

Copy link
Copy Markdown

Tested this branch (cordova-android 15.1.1-dev.0) with the repro app https://github.com/2zovd/cordova-backbutton-repro on an Android 16 (API 36) emulator, Pixel 9, WebView 153.

Screenshot 2026-10-13 at 11 21 35 AM
  • Back key, single presses: 1 event per press.
  • Edge swipe: 1 event per swipe.
  • 5 Back keys in a row via adb: 5 events, 23-44 ms apart.
  • Fast manual presses: one event per press, the smallest gap is about 150 ms. The repro page groups events closer than 500 ms into one "press", so these show up as one red line, but it is one event per key press.

With 15.1.0 the same app gets 2 events per Back key press, 0.4 ms apart. I did not see such pairs with this branch.

Thanks for the quick fix!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

backbutton fires twice for one Back key press on Android 16

4 participants