SDL: Android: control activity re-creation

From dfd80f3d762441e34e501c18a20b50c09fe3730f Mon Sep 17 00:00:00 2001
From: Sylvain <[EMAIL REDACTED]>
Date: Wed, 5 Apr 2023 21:54:36 +0200
Subject: [PATCH] Android: control activity re-creation

---
 WhatsNew.txt                                  |  1 +
 .../main/java/org/libsdl/app/SDLActivity.java | 22 ++++++++++++++
 docs/README-android.md                        |  7 +++++
 include/SDL3/SDL_hints.h                      | 13 +++++++++
 src/core/android/SDL_android.c                | 29 ++++++++++++++++++-
 5 files changed, 71 insertions(+), 1 deletion(-)

diff --git a/WhatsNew.txt b/WhatsNew.txt
index ea88419aa0f3..1e91ba8cb71a 100644
--- a/WhatsNew.txt
+++ b/WhatsNew.txt
@@ -29,3 +29,4 @@ General:
 * Added SDL_GetRenderVSync() to get vsync of the given renderer
 * Added SDL_PlayAudioDevice() to start audio playback
 * Added SDL_ConvertAudioSamples() to convert audio samples from one format to another
+* Added SDL_HINT_ANDROID_ALLOW_RECREATE_ACTIVITY hint to control re-creation of Android SDL activity.
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 de03ba790b28..db524b639c76 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
@@ -221,6 +221,8 @@ public enum NativeState {
 
     // This is what SDL runs in. It invokes SDL_main(), eventually
     protected static Thread mSDLThread;
+    protected static boolean mSDLMainFinished = false;
+    protected static boolean mActivityCreated = false;
 
     protected static SDLGenericMotionListener_API12 getMotionListener() {
         if (mMotionListener == null) {
@@ -324,6 +326,24 @@ protected void onCreate(Bundle savedInstanceState) {
         Log.v(TAG, "onCreate()");
         super.onCreate(savedInstanceState);
 
+
+        /* Control activity re-creation */
+        if (mSDLMainFinished || mActivityCreated) {
+              boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity();
+              if (mSDLMainFinished) {
+                  Log.v(TAG, "SDL main() finished");
+              }
+              if (allow_recreate) {
+                  Log.v(TAG, "activity re-created");
+              } else {
+                  Log.v(TAG, "activity finished");
+                  System.exit(0);
+                  return;
+              }
+        }
+
+        mActivityCreated = true;
+
         try {
             Thread.currentThread().setName("SDLActivity");
         } catch (Exception e) {
@@ -950,6 +970,7 @@ public static native void onNativeTouch(int touchDevId, int pointerFingerId,
     public static native void nativePermissionResult(int requestCode, boolean result);
     public static native void onNativeLocaleChanged();
     public static native void onNativeDarkModeChanged(boolean enabled);
+    public static native boolean nativeAllowRecreateActivity();
 
     /**
      * This method is called by SDL using JNI.
@@ -1902,6 +1923,7 @@ public void run() {
         if (SDLActivity.mSingleton != null && !SDLActivity.mSingleton.isFinishing()) {
             // Let's finish the Activity
             SDLActivity.mSDLThread = null;
+            SDLActivity.mSDLMainFinished = true;
             SDLActivity.mSingleton.finish();
         }  // else: Activity is already being destroyed
 
diff --git a/docs/README-android.md b/docs/README-android.md
index 4f6b75a0aad1..0c21ce8f14c9 100644
--- a/docs/README-android.md
+++ b/docs/README-android.md
@@ -218,6 +218,13 @@ You should not use the SDL renderer API while the app going in background:
    GL context is restored, and the SDL renderer API is available (unless you
    receive SDL_EVENT_RENDER_DEVICE_RESET).
 
+Activity lifecyle
+================================================================================
+
+You can control activity re-creation (eg. onCreate()) behaviour. This allows to keep
+or re-initialize java and native static datas, see SDL_hints.h:
+- SDL_HINT_ANDROID_ALLOW_RECREATE_ACTIVITY
+
 Mouse / Touch events
 ================================================================================
 
diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 70f95957bed4..09830b563c86 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -145,6 +145,19 @@ extern "C" {
  */
 #define SDL_HINT_ANDROID_TRAP_BACK_BUTTON "SDL_ANDROID_TRAP_BACK_BUTTON"
 
+/**
+ * \brief A variable to control whether SDL activity is allowed to be re-created.
+ *        If so, java static datas and static datas from native libraries remain with their current values.
+ *        When not allowed, the activity terminates with exit(0) to be fully re-initialized afterward.
+ *
+ * The variable can be set to the following values:
+ *   "0"       - Not allowed. (default)
+ *   "1"       - Allowed.
+ *
+ * The value of this hint is used at runtime, so it can be changed at any time.
+ */
+#define SDL_HINT_ANDROID_ALLOW_RECREATE_ACTIVITY "SDL_ANDROID_ALLOW_RECREATE_ACTIVITY"
+
 /**
  *  \brief Specify an application name.
  *
diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c
index 028bd1fba2ff..6a6cc83058fe 100644
--- a/src/core/android/SDL_android.c
+++ b/src/core/android/SDL_android.c
@@ -33,6 +33,7 @@
 #include "../../joystick/android/SDL_sysjoystick_c.h"
 #include "../../haptic/android/SDL_syshaptic_c.h"
 #include "../../hidapi/android/hid.h"
+#include "../../SDL_hints_c.h"
 
 #include <android/log.h>
 #include <android/configuration.h>
@@ -166,6 +167,9 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativePermissionResult)(
     JNIEnv *env, jclass cls,
     jint requestCode, jboolean result);
 
+JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(nativeAllowRecreateActivity)(
+    JNIEnv *env, jclass jcls);
+
 static JNINativeMethod SDLActivity_tab[] = {
     { "nativeGetVersion", "()Ljava/lang/String;", SDL_JAVA_INTERFACE(nativeGetVersion) },
     { "nativeSetupJNI", "()I", SDL_JAVA_INTERFACE(nativeSetupJNI) },
@@ -197,7 +201,8 @@ static JNINativeMethod SDLActivity_tab[] = {
     { "nativeSetenv", "(Ljava/lang/String;Ljava/lang/String;)V", SDL_JAVA_INTERFACE(nativeSetenv) },
     { "onNativeOrientationChanged", "(I)V", SDL_JAVA_INTERFACE(onNativeOrientationChanged) },
     { "nativeAddTouch", "(ILjava/lang/String;)V", SDL_JAVA_INTERFACE(nativeAddTouch) },
-    { "nativePermissionResult", "(IZ)V", SDL_JAVA_INTERFACE(nativePermissionResult) }
+    { "nativePermissionResult", "(IZ)V", SDL_JAVA_INTERFACE(nativePermissionResult) },
+    { "nativeAllowRecreateActivity", "()Z", SDL_JAVA_INTERFACE(nativeAllowRecreateActivity) },
 };
 
 /* Java class SDLInputConnection */
@@ -375,6 +380,9 @@ static void Internal_Android_Destroy_AssetManager(void);
 static AAssetManager *asset_manager = NULL;
 static jobject javaAssetManagerRef = 0;
 
+/* Re-create activity hint */
+static SDL_AtomicInt bAllowRecreateActivity;
+
 /*******************************************************************************
                  Functions called by JNI
 *******************************************************************************/
@@ -525,6 +533,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
     register_methods(env, "org/libsdl/app/SDLAudioManager", SDLAudioManager_tab, SDL_arraysize(SDLAudioManager_tab));
     register_methods(env, "org/libsdl/app/SDLControllerManager", SDLControllerManager_tab, SDL_arraysize(SDLControllerManager_tab));
     register_methods(env, "org/libsdl/app/HIDDeviceManager", HIDDeviceManager_tab, SDL_arraysize(HIDDeviceManager_tab));
+    SDL_AtomicSet(&bAllowRecreateActivity, SDL_FALSE);
 
     return JNI_VERSION_1_4;
 }
@@ -731,6 +740,21 @@ typedef int (*SDL_main_func)(int argc, char *argv[]);
 
 static int run_count = 1;
 
+static void SDLCALL SDL_AllowRecreateActivityChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
+{
+    if (SDL_GetStringBoolean(hint, SDL_FALSE)) {
+        SDL_AtomicSet(&bAllowRecreateActivity, SDL_TRUE);
+    } else {
+        SDL_AtomicSet(&bAllowRecreateActivity, SDL_FALSE);
+    }
+}
+
+JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(nativeAllowRecreateActivity)(
+    JNIEnv *env, jclass jcls)
+{
+    return SDL_AtomicGet(&bAllowRecreateActivity);
+}
+
 /* Start up the SDL app */
 JNIEXPORT int JNICALL SDL_JAVA_INTERFACE(nativeRunMain)(JNIEnv *env, jclass cls, jstring library, jstring function, jobject array)
 {
@@ -739,6 +763,9 @@ JNIEXPORT int JNICALL SDL_JAVA_INTERFACE(nativeRunMain)(JNIEnv *env, jclass cls,
     void *library_handle;
 
     __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativeRunMain() %d time", run_count);
+    if (run_count == 1) {
+        SDL_AddHintCallback(SDL_HINT_ANDROID_ALLOW_RECREATE_ACTIVITY, SDL_AllowRecreateActivityChanged, NULL);
+    }
     run_count += 1;
 
     /* Save JNIEnv of SDLThread */