SDL: android: handle sensor registration synchronized in one place

From 47c8dcc968b29db7164e3ee5b5a8d8874ef7d26b Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 20 May 2026 14:57:36 -0700
Subject: [PATCH] android: handle sensor registration synchronized in one place

---
 .../org/libsdl/app/SDLControllerManager.java  | 16 +++-------
 .../java/org/libsdl/app/SDLSensorManager.java | 32 +++++++++++++++++++
 .../main/java/org/libsdl/app/SDLSurface.java  |  6 ++--
 3 files changed, 40 insertions(+), 14 deletions(-)
 create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLSensorManager.java

diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java b/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java
index bce15e9de228a..a41467c61d06d 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java
@@ -91,13 +91,7 @@ static void joystickSetLED(int device_id, int red, int green, int blue) {
      * This method is called by SDL using JNI.
      */
     static void joystickSetSensorsEnabled(int device_id, boolean enabled) {
-        // Run this on the UI thread so we don't race with enableSensor() in SDLSurface.java
-        SDL.getContext().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                mJoystickHandler.setSensorsEnabled(device_id, enabled);
-            }
-        });
+        mJoystickHandler.setSensorsEnabled(device_id, enabled);
     }
 
     /**
@@ -558,17 +552,17 @@ void setSensorsEnabled(int device_id, boolean enabled) {
         }
         if (enabled) {
             if (joystick.accelerometerSensor != null) {
-                joystick.sensorManager.registerListener(joystick.sensorListener, joystick.accelerometerSensor, SensorManager.SENSOR_DELAY_GAME, null);
+                SDLSensorManager.registerListener(joystick.sensorManager, joystick.sensorListener, joystick.accelerometerSensor, SensorManager.SENSOR_DELAY_GAME);
             }
             if (joystick.gyroscopeSensor != null) {
-                joystick.sensorManager.registerListener(joystick.sensorListener, joystick.gyroscopeSensor, SensorManager.SENSOR_DELAY_GAME, null);
+                SDLSensorManager.registerListener(joystick.sensorManager, joystick.sensorListener, joystick.gyroscopeSensor, SensorManager.SENSOR_DELAY_GAME);
             }
         } else {
             if (joystick.accelerometerSensor != null) {
-                joystick.sensorManager.unregisterListener(joystick.sensorListener, joystick.accelerometerSensor);
+                SDLSensorManager.unregisterListener(joystick.sensorManager, joystick.sensorListener, joystick.accelerometerSensor);
             }
             if (joystick.gyroscopeSensor != null) {
-                joystick.sensorManager.unregisterListener(joystick.sensorListener, joystick.gyroscopeSensor);
+                SDLSensorManager.unregisterListener(joystick.sensorManager, joystick.sensorListener, joystick.gyroscopeSensor);
             }
         }
     }
diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLSensorManager.java b/android-project/app/src/main/java/org/libsdl/app/SDLSensorManager.java
new file mode 100644
index 0000000000000..586e3fab6e8fd
--- /dev/null
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLSensorManager.java
@@ -0,0 +1,32 @@
+package org.libsdl.app;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+
+// This class coordinates synchronized access to sensor manager registration
+//
+// This prevents a java.util.ConcurrentModificationException exception on
+// Android 16, specifically on the Samsung Tab S9 Ultra.
+
+class SDLSensorManager
+{
+    static private SDLSensorManager mManager = new SDLSensorManager();
+
+    public static void registerListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) {
+        mManager.RegisterListener(manager, listener, sensor, samplingPeriodUs);
+    }
+
+    public static void unregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) {
+        mManager.UnregisterListener(manager, listener, sensor);
+    }
+
+    private synchronized void RegisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) {
+        manager.registerListener(listener, sensor, samplingPeriodUs, null);
+    }
+
+    private synchronized void UnregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) {
+        manager.unregisterListener(listener, sensor);
+    }
+}
+
diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java b/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java
index dedc00b78a8c0..196cf04ec2fb5 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java
@@ -328,11 +328,11 @@ public boolean onTouch(View v, MotionEvent event) {
     protected void enableSensor(int sensortype, boolean enabled) {
         // TODO: This uses getDefaultSensor - what if we have >1 accels?
         if (enabled) {
-            mSensorManager.registerListener(this,
+            SDLSensorManager.registerListener(mSensorManager, this,
                             mSensorManager.getDefaultSensor(sensortype),
-                            SensorManager.SENSOR_DELAY_GAME, null);
+                            SensorManager.SENSOR_DELAY_GAME);
         } else {
-            mSensorManager.unregisterListener(this,
+            SDLSensorManager.unregisterListener(mSensorManager, this,
                             mSensorManager.getDefaultSensor(sensortype));
         }
     }