From 9064bc0792f48cd8b99b2ff7cb845f56bbd98e15 Mon Sep 17 00:00:00 2001 From: Manuel Beck Date: Fri, 13 Mar 2026 19:43:15 +0100 Subject: [PATCH] fix(android): use AndroidX `OnBackPressedCallback` instead of `OnBackInvokedCallback` (#1903) - `CoreAndroid.java` referenced `OnBackInvokedCallback` directly (including lambda generation), which can trigger class resolution on runtimes older than API 33, where the class is not available. This was introduced in PR https://github.com/apache/cordova-android/pull/1831 - Replaced `OnBackInvokedCallback` with AndroidX `OnBackPressedCallback` - Fixes error message: `java.lang.Class: java.lang.NoClassDefFoundError: Failed resolution of: Landroid/window/OnBackInvokedCallback;` on older Android versions --------- Generated-By: GPT-5.3-Codex, GitHub Copilot Chat Co-authored-by: Norman Breau --- .../src/org/apache/cordova/CoreAndroid.java | 77 +++++++++++++------ 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/framework/src/org/apache/cordova/CoreAndroid.java b/framework/src/org/apache/cordova/CoreAndroid.java index 2550b9b2..ff524189 100755 --- a/framework/src/org/apache/cordova/CoreAndroid.java +++ b/framework/src/org/apache/cordova/CoreAndroid.java @@ -32,11 +32,12 @@ import android.os.Build; import android.content.IntentFilter; import android.telephony.TelephonyManager; import android.view.KeyEvent; -import android.window.OnBackInvokedCallback; -import android.window.OnBackInvokedDispatcher; import java.util.HashMap; +import androidx.activity.OnBackPressedCallback; +import androidx.activity.OnBackPressedDispatcherOwner; + /** * This class exposes methods in Cordova that can be called from JavaScript. */ @@ -48,7 +49,7 @@ public class CoreAndroid extends CordovaPlugin { private CallbackContext messageChannel; private PluginResult pendingResume; private PluginResult pendingPause; - private OnBackInvokedCallback backCallback; + private OnBackPressedCallback backCallback; private final Object messageChannelLock = new Object(); private final Object backButtonHandlerLock = new Object(); @@ -253,30 +254,56 @@ public class CoreAndroid extends CordovaPlugin { */ public void overrideBackbutton(boolean override) { LOG.i("App", "WARNING: Back Button Default Behavior will be overridden. The backbutton event will be fired!"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { - if (override) { - synchronized (backButtonHandlerLock) { - if (backCallback == null) { - // The callback is intentionally empty. Since API 36, intercepting the back button is ignored, which means - // the onDispatchKeyEvent boolean result won't actually stop native from consuming the back button and doing - // it's own logic, UNLESS if there is an OnBackInvokedCallback registered on the dispatcher. - // The key dispatch events will still fire, which still handles propagating back button events to the webview. - // See https://developer.android.com/about/versions/16/behavior-changes-16#predictive-back for more info on the caveat. - backCallback = () -> {}; - this.cordova.getActivity().getOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, backCallback); - } - } - } else { - synchronized (backButtonHandlerLock) { - if (backCallback != null) { - this.cordova.getActivity().getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(backCallback); - backCallback = null; - } - } - } + if (cordova.getActivity() == null) { + return; } - webView.setButtonPlumbedToJs(KeyEvent.KEYCODE_BACK, override); + final boolean shouldOverride = override; + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (shouldOverride) { + synchronized (backButtonHandlerLock) { + if (backCallback == null) { + registerBackPressedCallback(); + } + } + } else { + synchronized (backButtonHandlerLock) { + if (backCallback != null) { + unregisterBackPressedCallback(); + } + } + } + + webView.setButtonPlumbedToJs(KeyEvent.KEYCODE_BACK, shouldOverride); + } + }); + } + + /** + * Registers an AndroidX back callback so Cordova can keep routing back presses through its + * existing key dispatch path across Android versions without directly referencing newer + * platform-only back APIs. + */ + private void registerBackPressedCallback() { + final OnBackPressedDispatcherOwner backPressedDispatcherOwner = this.cordova.getActivity(); + + backCallback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + // Intentionally empty. + // On modern Android versions, registering a callback keeps back handling + // routed through Cordova's existing key dispatch path. + } + }; + + backPressedDispatcherOwner.getOnBackPressedDispatcher().addCallback(backPressedDispatcherOwner, backCallback); + } + + private void unregisterBackPressedCallback() { + backCallback.remove(); + backCallback = null; } /**