Skip to content

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

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

GitToTheHub wants to merge 2 commits into
apache:masterfrom
GitToTheHub:fix/android-backbutton-once-2028

Conversation

@GitToTheHub

@GitToTheHub GitToTheHub commented Oct 12, 2026 •

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

A back press can reach Cordova in two ways:

  • Back-press callback: CoreAndroid registers an OnBackPressedCallback while the back button is overridden, that is while a JavaScript backbutton listener exists.
  • Back key event: the WebView's key event handling (onDispatchKeyEvent). This also works without a backbutton listener.

Depending on the Android version, one press arrives through one or both ways. On Android 16 it can arrive through both, which caused the duplicate.

Replace synthetic DOWN/UP keys with a shared back-action handler. Callbacks and back 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.

The new CordovaWebView.handleBackButton() interface method is called by CoreAndroid from the back-press callback.

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.
  • The same six tests also pass on Android 17 (API 37), repeated runs on Android 15, 16 and 17 all pass.
  • A sample app with a backbutton listener produces one event per real Back key press on Android 12 to 17 (checked with input keyevent), and per edge swipe on Android 16 and 17. Consecutive key presses about 21–23 ms apart were checked on Android 15 and 16. How a press arrives differs by version: Android 12 to 15 deliver key events only, Android 16 delivers the callback just before the key-up (which is ignored), Android 17 delivers the callback only.
  • Without a backbutton listener, Back behaves the same on Android 16 and 17 as before this PR. Checked by comparing with the code before the PR, using real Back key presses.
  • 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 was not run. The six tests were not run on Android 12 to 14.

On Android 11 and older, the test app crashes at startup (InflateException in the splash screen layout), because its theme lacks splashScreenIconSize. This is unrelated to this PR. With that attribute temporarily added to the test theme (not part of this PR):

  • Android 10: the six tests passed in 5 of 5 runs.
  • Android 7.1: the six tests fail intermittently. The cause is a race in which the page's first exec() calls, such as overrideBackbutton, are dropped as "from previous page load". A separate test that only waits for the back override to arrive failed in 2 to 23 of 60 page loads with the code from before this PR as well, so the PR does not cause it.

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

codecov-commenter commented Oct 12, 2026 •

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 (0769f65).
⚠️ Report is 1 commits behind head on master.

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.

Comment thread framework/src/org/apache/cordova/CordovaWebViewImpl.java Fixed
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!

@GitToTheHub

Copy link
Copy Markdown
Contributor Author

Thanks for the reply and tests :)

@GitToTheHub

Copy link
Copy Markdown
Contributor Author

If no one has any objections, I would merge this.

Document the two ways a back press reaches Cordova (back-press callback and
back key events) in CordovaWebView, CordovaWebViewImpl and CoreAndroid, and
add comments to BackButtonDispatchTest.

Make the history navigation test wait for the page load to finish after going
back instead of evaluating JavaScript during the navigation, which could drop
the result callback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@GitToTheHub

GitToTheHub commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

I documented the back button behaviour in cordova-android. If no one has any objections I would merge this the next days.

private boolean performBackButtonAction() {
// 1. A custom view (for example a full-screen video) is showing: close it.
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.

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