SDL: Use the modern OnBackInvokedCallback method to trap the back button on newer Android devices (#16052)

From 412a7c5db639399b1bbaa4516d56f390884ea28b Mon Sep 17 00:00:00 2001
From: Rachel Blackman <[EMAIL REDACTED]>
Date: Mon, 27 Jul 2026 15:43:23 -0700
Subject: [PATCH] Use the modern OnBackInvokedCallback method to trap the back
 button on newer Android devices (#16052)

---
 .../app/src/main/AndroidManifest.xml          |  2 +-
 .../main/java/org/libsdl/app/SDLActivity.java | 44 +++++++++++++++++++
 src/core/android/SDL_android.c                | 15 ++++++-
 src/core/android/SDL_android.h                |  1 +
 src/video/android/SDL_androidvideo.c          | 14 ++++++
 5 files changed, 74 insertions(+), 2 deletions(-)

diff --git a/android-project/app/src/main/AndroidManifest.xml b/android-project/app/src/main/AndroidManifest.xml
index 1111a223dcf28..93e851286646a 100644
--- a/android-project/app/src/main/AndroidManifest.xml
+++ b/android-project/app/src/main/AndroidManifest.xml
@@ -71,7 +71,7 @@
         android:icon="@mipmap/ic_launcher"
         android:allowBackup="true"
         android:theme="@style/AppTheme"
-        android:enableOnBackInvokedCallback="false"
+        android:enableOnBackInvokedCallback="true"
         android:hardwareAccelerated="true" >
 
         <!-- Example of setting SDL hints from AndroidManifest.xml:
diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
index ed1ffd509143a..b495e8773dda6 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
@@ -24,6 +24,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.LocaleList;
+import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
 import android.provider.DocumentsContract;
@@ -50,6 +51,8 @@
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 import android.widget.Toast;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import java.io.FileNotFoundException;
 import java.util.ArrayList;
@@ -784,6 +787,47 @@ public void onBackPressed() {
         }
     }
 
+    static OnBackInvokedCallback backButtonCallback = new OnBackInvokedCallback() {
+        Handler mBackKeyHandler;
+
+        @Override
+        public void onBackInvoked() {
+            if (mBackKeyHandler == null) {
+                mBackKeyHandler = new Handler(Looper.getMainLooper());
+            }
+
+            onNativeKeyDown(KeyEvent.KEYCODE_BACK);
+            mBackKeyHandler.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    onNativeKeyUp(KeyEvent.KEYCODE_BACK);
+                }
+            }, 500);
+        }
+    };
+
+    static boolean mBackKeyTrapEnabled = false;
+    public static void setBackButtonTrapEnabled(boolean enabled) {
+
+        if ( Build.VERSION.SDK_INT < 33 ) {
+            // We're old enough that we just use the old onBackPressed behavior.
+            return;
+        }
+
+        // Check so we don't register the same callback twice.
+        if (enabled == mBackKeyTrapEnabled) {
+            return;
+        }
+
+        if (enabled) {
+            mSingleton.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, backButtonCallback);
+        }
+        else {
+            mSingleton.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(backButtonCallback);
+        }
+        mBackKeyTrapEnabled = enabled;
+    }
+
     // File dialog types
     private static final int SDL_FILEDIALOG_OPENFILE = 0;
     private static final int SDL_FILEDIALOG_SAVEFILE = 1;
diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c
index eedb4af4dfff6..de4d39120f538 100644
--- a/src/core/android/SDL_android.c
+++ b/src/core/android/SDL_android.c
@@ -101,6 +101,7 @@ static jmethodID midManualBackButton;
 static jmethodID midShowFileDialog;
 #endif // !SDL_DIALOG_DISABLED
 static jmethodID midGetPreferredLocales;
+static jmethodID midSetBackButtonTrapEnabled;
 
 #ifndef SDL_VIDEO_DISABLED
 // Video/surface method signatures
@@ -681,6 +682,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl
     midShowFileDialog = (*env)->GetStaticMethodID(env, mActivityClass, "showFileDialog", "([Ljava/lang/String;ZILjava/lang/String;I)Z");
 #endif // !SDL_DIALOG_DISABLED
     midGetPreferredLocales = (*env)->GetStaticMethodID(env, mActivityClass, "getPreferredLocales", "()Ljava/lang/String;");
+    midSetBackButtonTrapEnabled = (*env)->GetStaticMethodID(env, mActivityClass, "setBackButtonTrapEnabled", "(Z)V");
 
     if (!midGetContext ||
         !midGetDeviceFormFactor ||
@@ -698,7 +700,8 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl
 #ifndef SDL_DIALOG_DISABLED
         !midShowFileDialog ||
 #endif
-        !midGetPreferredLocales) {
+        !midGetPreferredLocales ||
+        !midSetBackButtonTrapEnabled) {
         __android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some core Java callbacks, do you have the latest version of SDLActivity.java?");
     }
 
@@ -3714,4 +3717,14 @@ bool Android_JNI_ShowFileDialog(
 }
 #endif // !SDL_DIALOG_DISABLED
 
+void Android_JNI_SetBackButtonTrapActive(bool enabled)
+{
+    if (!midSetBackButtonTrapEnabled) {
+        return;
+    }
+
+    JNIEnv *env = Android_JNI_GetEnv();
+    (*env)->CallStaticVoidMethod(env, mActivityClass, midSetBackButtonTrapEnabled, enabled);
+}
+
 #endif // SDL_PLATFORM_ANDROID
diff --git a/src/core/android/SDL_android.h b/src/core/android/SDL_android.h
index b6c6a31438f4b..01074612e9c65 100644
--- a/src/core/android/SDL_android.h
+++ b/src/core/android/SDL_android.h
@@ -60,6 +60,7 @@ void Android_LockActivityMutex(void);
 void Android_UnlockActivityMutex(void);
 
 void Android_SetAllowRecreateActivity(bool enabled);
+void Android_JNI_SetBackButtonTrapActive(bool enabled);
 
 #ifndef SDL_VIDEO_DISABLED
 #include <EGL/eglplatform.h>
diff --git a/src/video/android/SDL_androidvideo.c b/src/video/android/SDL_androidvideo.c
index d868e6960e264..2157d1168e013 100644
--- a/src/video/android/SDL_androidvideo.c
+++ b/src/video/android/SDL_androidvideo.c
@@ -28,6 +28,7 @@
 #include "../SDL_pixels_c.h"
 #include "../../events/SDL_events_c.h"
 #include "../../events/SDL_windowevents_c.h"
+#include "../../SDL_hints_c.h"
 
 #include "SDL_androidvideo.h"
 #include "SDL_androidgl.h"
@@ -81,6 +82,16 @@ static void Android_DeleteDevice(SDL_VideoDevice *device)
     SDL_free(device);
 }
 
+// Input is part of video for Android, so this goes here.
+static void SDLCALL Android_Video_InternalHintCallback(
+    void *userdata,
+    const char *name,
+    const char *oldvalue,
+    const char *newvalue)
+{
+    Android_JNI_SetBackButtonTrapActive(SDL_GetStringBoolean(newvalue, false));
+}
+
 static SDL_VideoDevice *Android_CreateDevice(void)
 {
     SDL_VideoDevice *device;
@@ -185,6 +196,8 @@ bool Android_VideoInit(SDL_VideoDevice *_this)
     display->current_orientation = Android_JNI_GetDisplayCurrentOrientation();
     display->content_scale = Android_ScreenDensity;
 
+    SDL_AddHintCallback(SDL_HINT_ANDROID_TRAP_BACK_BUTTON, Android_Video_InternalHintCallback, 0);
+
     Android_InitTouch();
 
     Android_InitMouse();
@@ -195,6 +208,7 @@ bool Android_VideoInit(SDL_VideoDevice *_this)
 
 void Android_VideoQuit(SDL_VideoDevice *_this)
 {
+    SDL_RemoveHintCallback(SDL_HINT_ANDROID_TRAP_BACK_BUTTON, Android_Video_InternalHintCallback, 0);
     Android_QuitMouse();
     Android_QuitTouch();
 }