SDL: Separate android initialization from Activity (#11891)

https://github.com/libsdl-org/SDL/commit/d14c93c4b11c116ab897f004ca15e726a77c4469

From d14c93c4b11c116ab897f004ca15e726a77c4469 Mon Sep 17 00:00:00 2001
From: Xander <[EMAIL REDACTED]>
Date: Fri, 10 Jan 2025 23:05:58 +0000
Subject: [PATCH] Separate android initialization from Activity (#11891)

---
 android-project/app/build.gradle              |    5 +
 android-project/app/proguard-rules.pro        |    2 +-
 .../java/org/libsdl/app/ActivityHook.java     |   15 +
 .../app/src/main/java/org/libsdl/app/SDL.java |    4 +-
 .../main/java/org/libsdl/app/SDLActivity.java | 2120 +---------------
 .../org/libsdl/app/SDLActivityComponent.java  | 2201 +++++++++++++++++
 .../org/libsdl/app/SDLComponentReceiver.java  |    5 +
 .../org/libsdl/app/SDLControllerManager.java  |   19 +-
 .../java/org/libsdl/app/SDLDummyEdit.java     |    7 +-
 .../org/libsdl/app/SDLInputConnection.java    |    5 +-
 .../main/java/org/libsdl/app/SDLSurface.java  |   57 +-
 build-scripts/release-info.json               |    2 +-
 build-scripts/test-versioning.sh              |   10 +-
 build-scripts/update-version.sh               |    6 +-
 docs/README-android.md                        |    3 +-
 src/core/android/SDL_android.c                |   10 +-
 16 files changed, 2317 insertions(+), 2154 deletions(-)
 create mode 100644 android-project/app/src/main/java/org/libsdl/app/ActivityHook.java
 create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLActivityComponent.java
 create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLComponentReceiver.java

diff --git a/android-project/app/build.gradle b/android-project/app/build.gradle
index 8946de66778aa..50a44c99aa76e 100644
--- a/android-project/app/build.gradle
+++ b/android-project/app/build.gradle
@@ -55,6 +55,11 @@ android {
     lint {
         abortOnError false
     }
+    compileOptions {
+        // our minSdk, lollipop (API 21) uses java 7
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
 }
 
 dependencies {
diff --git a/android-project/app/proguard-rules.pro b/android-project/app/proguard-rules.pro
index fc0a4f5c9264b..e29a7e07d9851 100644
--- a/android-project/app/proguard-rules.pro
+++ b/android-project/app/proguard-rules.pro
@@ -16,7 +16,7 @@
 #   public *;
 #}
 
--keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivity {
+-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivityComponent {
     java.lang.String nativeGetHint(java.lang.String); # Java-side doesn't use this, so it gets minified, but C-side still tries to register it
     java.lang.String clipboardGetText();
     boolean clipboardHasText();
diff --git a/android-project/app/src/main/java/org/libsdl/app/ActivityHook.java b/android-project/app/src/main/java/org/libsdl/app/ActivityHook.java
new file mode 100644
index 0000000000000..d64adf5a4373e
--- /dev/null
+++ b/android-project/app/src/main/java/org/libsdl/app/ActivityHook.java
@@ -0,0 +1,15 @@
+package org.libsdl.app;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation for {@link SDLActivityComponent} methods that correspond to
+ * events in {@link android.app.Activity}.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.METHOD)
+public @interface ActivityHook {
+}
diff --git a/android-project/app/src/main/java/org/libsdl/app/SDL.java b/android-project/app/src/main/java/org/libsdl/app/SDL.java
index b132fea088b74..d9a73a39c43a4 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDL.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDL.java
@@ -13,7 +13,7 @@ public class SDL {
     // This function should be called first and sets up the native code
     // so it can call into the Java classes
     public static void setupJNI() {
-        SDLActivity.nativeSetupJNI();
+        SDLActivityComponent.nativeSetupJNI();
         SDLAudioManager.nativeSetupJNI();
         SDLControllerManager.nativeSetupJNI();
     }
@@ -22,7 +22,7 @@ public static void setupJNI() {
     public static void initialize() {
         setContext(null);
 
-        SDLActivity.initialize();
+        SDLActivityComponent.initialize();
         SDLAudioManager.initialize();
         SDLControllerManager.initialize();
     }
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 8a1aa690b497e..dd514a8b8e549 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
@@ -1,732 +1,90 @@
 package org.libsdl.app;
 
 import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.UiModeManager;
-import android.content.ActivityNotFoundException;
-import android.content.ClipboardManager;
-import android.content.ClipData;
-import android.content.Context;
-import android.content.DialogInterface;
 import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
 import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.hardware.Sensor;
-import android.net.Uri;
-import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.os.ParcelFileDescriptor;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.Display;
-import android.view.Gravity;
-import android.view.InputDevice;
 import android.view.KeyEvent;
-import android.view.PointerIcon;
-import android.view.Surface;
 import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowManager;
-import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputMethodManager;
-import android.webkit.MimeTypeMap;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-import android.widget.Toast;
 
-import java.io.FileNotFoundException;
-import java.util.ArrayList;
-import java.util.Hashtable;
-import java.util.Locale;
+public abstract class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener, SDLComponentReceiver {
+    private SDLActivityComponent component;
 
-
-/**
-    SDL Activity
-*/
-public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener {
-    private static final String TAG = "SDL";
-    private static final int SDL_MAJOR_VERSION = 3;
-    private static final int SDL_MINOR_VERSION = 1;
-    private static final int SDL_MICRO_VERSION = 9;
-/*
-    // Display InputType.SOURCE/CLASS of events and devices
-    //
-    // SDLActivity.debugSource(device.getSources(), "device[" + device.getName() + "]");
-    // SDLActivity.debugSource(event.getSource(), "event");
-    public static void debugSource(int sources, String prefix) {
-        int s = sources;
-        int s_copy = sources;
-        String cls = "";
-        String src = "";
-        int tst = 0;
-        int FLAG_TAINTED = 0x80000000;
-
-        if ((s & InputDevice.SOURCE_CLASS_BUTTON) != 0)     cls += " BUTTON";
-        if ((s & InputDevice.SOURCE_CLASS_JOYSTICK) != 0)   cls += " JOYSTICK";
-        if ((s & InputDevice.SOURCE_CLASS_POINTER) != 0)    cls += " POINTER";
-        if ((s & InputDevice.SOURCE_CLASS_POSITION) != 0)   cls += " POSITION";
-        if ((s & InputDevice.SOURCE_CLASS_TRACKBALL) != 0)  cls += " TRACKBALL";
-
-
-        int s2 = s_copy & ~InputDevice.SOURCE_ANY; // keep class bits
-        s2 &= ~(  InputDevice.SOURCE_CLASS_BUTTON
-                | InputDevice.SOURCE_CLASS_JOYSTICK
-                | InputDevice.SOURCE_CLASS_POINTER
-                | InputDevice.SOURCE_CLASS_POSITION
-                | InputDevice.SOURCE_CLASS_TRACKBALL);
-
-        if (s2 != 0) cls += "Some_Unknown";
-
-        s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class;
-
-        if (Build.VERSION.SDK_INT >= 23) {
-            tst = InputDevice.SOURCE_BLUETOOTH_STYLUS;
-            if ((s & tst) == tst) src += " BLUETOOTH_STYLUS";
-            s2 &= ~tst;
-        }
-
-        tst = InputDevice.SOURCE_DPAD;
-        if ((s & tst) == tst) src += " DPAD";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_GAMEPAD;
-        if ((s & tst) == tst) src += " GAMEPAD";
-        s2 &= ~tst;
-
-        if (Build.VERSION.SDK_INT >= 21) {
-            tst = InputDevice.SOURCE_HDMI;
-            if ((s & tst) == tst) src += " HDMI";
-            s2 &= ~tst;
-        }
-
-        tst = InputDevice.SOURCE_JOYSTICK;
-        if ((s & tst) == tst) src += " JOYSTICK";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_KEYBOARD;
-        if ((s & tst) == tst) src += " KEYBOARD";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_MOUSE;
-        if ((s & tst) == tst) src += " MOUSE";
-        s2 &= ~tst;
-
-        if (Build.VERSION.SDK_INT >= 26) {
-            tst = InputDevice.SOURCE_MOUSE_RELATIVE;
-            if ((s & tst) == tst) src += " MOUSE_RELATIVE";
-            s2 &= ~tst;
-
-            tst = InputDevice.SOURCE_ROTARY_ENCODER;
-            if ((s & tst) == tst) src += " ROTARY_ENCODER";
-            s2 &= ~tst;
-        }
-        tst = InputDevice.SOURCE_STYLUS;
-        if ((s & tst) == tst) src += " STYLUS";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_TOUCHPAD;
-        if ((s & tst) == tst) src += " TOUCHPAD";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_TOUCHSCREEN;
-        if ((s & tst) == tst) src += " TOUCHSCREEN";
-        s2 &= ~tst;
-
-        if (Build.VERSION.SDK_INT >= 18) {
-            tst = InputDevice.SOURCE_TOUCH_NAVIGATION;
-            if ((s & tst) == tst) src += " TOUCH_NAVIGATION";
-            s2 &= ~tst;
-        }
-
-        tst = InputDevice.SOURCE_TRACKBALL;
-        if ((s & tst) == tst) src += " TRACKBALL";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_ANY;
-        if ((s & tst) == tst) src += " ANY";
-        s2 &= ~tst;
-
-        if (s == FLAG_TAINTED) src += " FLAG_TAINTED";
-        s2 &= ~FLAG_TAINTED;
-
-        if (s2 != 0) src += " Some_Unknown";
-
-        Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src);
-    }
-*/
-
-    public static boolean mIsResumedCalled, mHasFocus;
-    public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24  /* Android 7.0 (N) */);
-
-    // Cursor types
-    // private static final int SDL_SYSTEM_CURSOR_NONE = -1;
-    private static final int SDL_SYSTEM_CURSOR_ARROW = 0;
-    private static final int SDL_SYSTEM_CURSOR_IBEAM = 1;
-    private static final int SDL_SYSTEM_CURSOR_WAIT = 2;
-    private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3;
-    private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4;
-    private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5;
-    private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6;
-    private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7;
-    private static final int SDL_SYSTEM_CURSOR_SIZENS = 8;
-    private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9;
-    private static final int SDL_SYSTEM_CURSOR_NO = 10;
-    private static final int SDL_SYSTEM_CURSOR_HAND = 11;
-    private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPLEFT = 12;
-    private static final int SDL_SYSTEM_CURSOR_WINDOW_TOP = 13;
-    private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPRIGHT = 14;
-    private static final int SDL_SYSTEM_CURSOR_WINDOW_RIGHT = 15;
-    private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMRIGHT = 16;
-    private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOM = 17;
-    private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMLEFT = 18;
-    private static final int SDL_SYSTEM_CURSOR_WINDOW_LEFT = 19;
-
-    protected static final int SDL_ORIENTATION_UNKNOWN = 0;
-    protected static final int SDL_ORIENTATION_LANDSCAPE = 1;
-    protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2;
-    protected static final int SDL_ORIENTATION_PORTRAIT = 3;
-    protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4;
-
-    protected static int mCurrentRotation;
-    protected static Locale mCurrentLocale;
-
-    // Handle the state of the native layer
-    public enum NativeState {
-           INIT, RESUMED, PAUSED
-    }
-
-    public static NativeState mNextNativeState;
-    public static NativeState mCurrentNativeState;
-
-    /** If shared libraries (e.g. SDL or the native application) could not be loaded. */
-    public static boolean mBrokenLibraries = true;
-
-    // Main components
-    protected static SDLActivity mSingleton;
-    protected static SDLSurface mSurface;
-    protected static SDLDummyEdit mTextEdit;
-    protected static boolean mScreenKeyboardShown;
-    protected static ViewGroup mLayout;
-    protected static SDLClipboardHandler mClipboardHandler;
-    protected static Hashtable<Integer, PointerIcon> mCursors;
-    protected static int mLastCursorID;
-    protected static SDLGenericMotionListener_API14 mMotionListener;
-    protected static HIDDeviceManager mHIDDeviceManager;
-
-    // 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;
-    private static SDLFileDialogState mFileDialogState = null;
-
-    protected static SDLGenericMotionListener_API14 getMotionListener() {
-        if (mMotionListener == null) {
-            if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
-                mMotionListener = new SDLGenericMotionListener_API26();
-            } else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
-                mMotionListener = new SDLGenericMotionListener_API24();
-            } else {
-                mMotionListener = new SDLGenericMotionListener_API14();
-            }
-        }
-
-        return mMotionListener;
-    }
-
-    /**
-     * The application entry point, called on a dedicated thread (SDLThread).
-     * The default implementation uses the getMainSharedObject() and getMainFunction() methods
-     * to invoke native code from the specified shared library.
-     * It can be overridden by derived classes.
-     */
-    protected void main() {
-        String library = SDLActivity.mSingleton.getMainSharedObject();
-        String function = SDLActivity.mSingleton.getMainFunction();
-        String[] arguments = SDLActivity.mSingleton.getArguments();
-
-        Log.v("SDL", "Running main function " + function + " from library " + library);
-        SDLActivity.nativeRunMain(library, function, arguments);
-        Log.v("SDL", "Finished main function");
-    }
-
-    /**
-     * This method returns the name of the shared object with the application entry point
-     * It can be overridden by derived classes.
-     */
-    protected String getMainSharedObject() {
-        String library;
-        String[] libraries = SDLActivity.mSingleton.getLibraries();
-        if (libraries.length > 0) {
-            library = "lib" + libraries[libraries.length - 1] + ".so";
-        } else {
-            library = "libmain.so";
-        }
-        return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
-    }
-
-    /**
-     * This method returns the name of the application entry point
-     * It can be overridden by derived classes.
-     */
-    protected String getMainFunction() {
-        return "SDL_main";
-    }
-
-    /**
-     * This method is called by SDL before loading the native shared libraries.
-     * It can be overridden to provide names of shared libraries to be loaded.
-     * The default implementation returns the defaults. It never returns null.
-     * An array returned by a new implementation must at least contain "SDL3".
-     * Also keep in mind that the order the libraries are loaded may matter.
-     * @return names of shared libraries to be loaded (e.g. "SDL3", "main").
-     */
     protected String[] getLibraries() {
-        return new String[] {
-            "SDL3",
-            // "SDL3_image",
-            // "SDL3_mixer",
-            // "SDL3_net",
-            // "SDL3_ttf",
-            "main"
-        };
-    }
-
-    // Load the .so
-    public void loadLibraries() {
-       for (String lib : getLibraries()) {
-          SDL.loadLibrary(lib, this);
-       }
+        return null;
     }
-
-    /**
-     * This method is called by SDL before starting the native application thread.
-     * It can be overridden to provide the arguments after the application name.
-     * The default implementation returns an empty array. It never returns null.
-     * @return arguments for the native application.
-     */
     protected String[] getArguments() {
-        return new String[0];
-    }
-
-    public static void initialize() {
-        // The static nature of the singleton and Android quirkyness force us to initialize everything here
-        // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
-        mSingleton = null;
-        mSurface = null;
-        mTextEdit = null;
-        mLayout = null;
-        mClipboardHandler = null;
-        mCursors = new Hashtable<Integer, PointerIcon>();
-        mLastCursorID = 0;
-        mSDLThread = null;
-        mIsResumedCalled = false;
-        mHasFocus = true;
-        mNextNativeState = NativeState.INIT;
-        mCurrentNativeState = NativeState.INIT;
-    }
-
-    protected SDLSurface createSDLSurface(Context context) {
-        return new SDLSurface(context);
+        return null;
     }
 
-    // Setup
     @Override
     protected void onCreate(Bundle savedInstanceState) {
-        Log.v(TAG, "Manufacturer: " + Build.MANUFACTURER);
-        Log.v(TAG, "Device: " + Build.DEVICE);
-        Log.v(TAG, "Model: " + Build.MODEL);
-        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) {
-            Log.v(TAG, "modify thread properties failed " + e.toString());
-        }
-
-        // Load shared libraries
-        String errorMsgBrokenLib = "";
-        try {
-            loadLibraries();
-            mBrokenLibraries = false; /* success */
-        } catch(UnsatisfiedLinkError e) {
-            System.err.println(e.getMessage());
-            mBrokenLibraries = true;
-            errorMsgBrokenLib = e.getMessage();
-        } catch(Exception e) {
-            System.err.println(e.getMessage());
-            mBrokenLibraries = true;
-            errorMsgBrokenLib = e.getMessage();
-        }
-
-        if (!mBrokenLibraries) {
-            String expected_version = String.valueOf(SDL_MAJOR_VERSION) + "." +
-                                      String.valueOf(SDL_MINOR_VERSION) + "." +
-                                      String.valueOf(SDL_MICRO_VERSION);
-            String version = nativeGetVersion();
-            if (!version.equals(expected_version)) {
-                mBrokenLibraries = true;
-                errorMsgBrokenLib = "SDL C/Java version mismatch (expected " + expected_version + ", got " + version + ")";
-            }
-        }
-
-        if (mBrokenLibraries) {
-            mSingleton = this;
-            AlertDialog.Builder dlgAlert  = new AlertDialog.Builder(this);
-            dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall."
-                  + System.getProperty("line.separator")
-                  + System.getProperty("line.separator")
-                  + "Error: " + errorMsgBrokenLib);
-            dlgAlert.setTitle("SDL Error");
-            dlgAlert.setPositiveButton("Exit",
-                new DialogInterface.OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialog,int id) {
-                        // if this button is clicked, close current activity
-                        SDLActivity.mSingleton.finish();
-                    }
-                });
-           dlgAlert.setCancelable(false);
-           dlgAlert.create().show();
-
-           return;
-        }
-
-
-        /* Control activity re-creation */
-        /* Robustness: check that the native code is run for the first time.
-         * (Maybe Activity was reset, but not the native code.) */
-        {
-            int run_count = SDLActivity.nativeCheckSDLThreadCounter(); /* get and increment a native counter */
-            if (run_count != 0) {
-                boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity();
-                if (allow_recreate) {
-                    Log.v(TAG, "activity re-created // run_count: " + run_count);
-                } else {
-                    Log.v(TAG, "activity finished // run_count: " + run_count);
-                    System.exit(0);
-                    return;
-                }
-            }
-        }
-
-        // Set up JNI
-        SDL.setupJNI();
-
-        // Initialize state
-        SDL.initialize();
-
-        // So we can call stuff from static callbacks
-        mSingleton = this;
-        SDL.setContext(this);
-
-        mClipboardHandler = new SDLClipboardHandler();
-
-        mHIDDeviceManager = HIDDeviceManager.acquire(this);
-
-        // Set up the surface
-        mSurface = createSDLSurface(this);
-
-        mLayout = new RelativeLayout(this);
-        mLayout.addView(mSurface);
-
-        // Get our current screen orientation and pass it down.
-        SDLActivity.nativeSetNaturalOrientation(SDLActivity.getNaturalOrientation());
-        mCurrentRotation = SDLActivity.getCurrentRotation();
-        SDLActivity.onNativeRotationChanged(mCurrentRotation);
-
-        try {
-            if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) {
-                mCurrentLocale = getContext().getResources().getConfiguration().locale;
-            } else {
-                mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0);
-            }
-        } catch(Exception ignored) {
+        component = new SDLActivityComponent(this);
+        String[] libraries = getLibraries();
+        if (libraries != null) {
+            component.setLibraries(libraries);
         }
-
-        switch (getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) {
-        case Configuration.UI_MODE_NIGHT_NO:
-            SDLActivity.onNativeDarkModeChanged(false);
-            break;
-        case Configuration.UI_MODE_NIGHT_YES:
-            SDLActivity.onNativeDarkModeChanged(true);
-            break;
-        }
-
-        setContentView(mLayout);
-
-        setWindowStyle(false);
-
-        getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
-
-        // Get filename from "Open with" of another application
-        Intent intent = getIntent();
-        if (intent != null && intent.getData() != null) {
-            String filename = intent.getData().getPath();
-            if (filename != null) {
-                Log.v(TAG, "Got filename: " + filename);
-                SDLActivity.onNativeDropFile(filename);
-            }
-        }
-    }
-
-    protected void pauseNativeThread() {
-        mNextNativeState = NativeState.PAUSED;
-        mIsResumedCalled = false;
-
-        if (SDLActivity.mBrokenLibraries) {
-            return;
-        }
-
-        SDLActivity.handleNativeState();
-    }
-
-    protected void resumeNativeThread() {
-        mNextNativeState = NativeState.RESUMED;
-        mIsResumedCalled = true;
-
-        if (SDLActivity.mBrokenLibraries) {
-           return;
+        String[] args = getArguments();
+        if (args != null) {
+            component.setArguments(args);
         }
 
-        SDLActivity.handleNativeState();
+        component.onCreate();
     }
 
-    // Events
     @Override
     protected void onPause() {
-        Log.v(TAG, "onPause()");
         super.onPause();
-
-        if (mHIDDeviceManager != null) {
-            mHIDDeviceManager.setFrozen(true);
-        }
-        if (!mHasMultiWindow) {
-            pauseNativeThread();
-        }
+        component.onPause();
     }
 
     @Override
     protected void onResume() {
-        Log.v(TAG, "onResume()");
         super.onResume();
-
-        if (mHIDDeviceManager != null) {
-            mHIDDeviceManager.setFrozen(false);
-        }
-        if (!mHasMultiWindow) {
-            resumeNativeThread();
-        }
+        component.onResume();
     }
 
     @Override
     protected void onStop() {
-        Log.v(TAG, "onStop()");
         super.onStop();
-        if (mHasMultiWindow) {
-            pauseNativeThread();
-        }
+        component.onStop();
     }
 
     @Override
     protected void onStart() {
-        Log.v(TAG, "onStart()");
         super.onStart();
-        if (mHasMultiWindow) {
-            resumeNativeThread();
-        }
-    }
-
-    public static int getNaturalOrientation() {
-        int result = SDL_ORIENTATION_UNKNOWN;
-
-        Activity activity = (Activity)getContext();
-        if (activity != null) {
-            Configuration config = activity.getResources().getConfiguration();
-            Display display = activity.getWindowManager().getDefaultDisplay();
-            int rotation = display.getRotation();
-            if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) &&
-                    config.orientation == Configuration.ORIENTATION_LANDSCAPE) ||
-                ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) &&
-                    config.orientation == Configuration.ORIENTATION_PORTRAIT)) {
-                result = SDL_ORIENTATION_LANDSCAPE;
-            } else {
-                result = SDL_ORIENTATION_PORTRAIT;
-            }
-        }
-        return result;
-    }
-
-    public static int getCurrentRotation() {
-        int result = 0;
-
-        Activity activity = (Activity)getContext();
-        if (activity != null) {
-            Display display = activity.getWindowManager().getDefaultDisplay();
-            switch (display.getRotation()) {
-                case Surface.ROTATION_0:
-                    result = 0;
-                    break;
-                case Surface.ROTATION_90:
-                    result = 90;
-                    break;
-                case Surface.ROTATION_180:
-                    result = 180;
-                    break;
-                case Surface.ROTATION_270:
-                    result = 270;
-                    break;
-            }
-        }
-        return result;
+        component.onStart();
     }
 
     @Override
     public void onWindowFocusChanged(boolean hasFocus) {
         super.onWindowFocusChanged(hasFocus);
-        Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
-
-        if (SDLActivity.mBrokenLibraries) {
-           return;
-        }
-
-        mHasFocus = hasFocus;
-        if (hasFocus) {
-           mNextNativeState = NativeState.RESUMED;
-           SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded();
-
-           SDLActivity.handleNativeState();
-           nativeFocusChanged(true);
-
-        } else {
-           nativeFocusChanged(false);
-           if (!mHasMultiWindow) {
-               mNextNativeState = NativeState.PAUSED;
-               SDLActivity.handleNativeState();
-           }
-        }
+        component.onWindowFocusChanged(hasFocus);
     }
 
     @Override
     public void onTrimMemory(int level) {
-        Log.v(TAG, "onTrimMemory()");
         super.onTrimMemory(level);
-
-        if (SDLActivity.mBrokenLibraries) {
-           return;
-        }
-
-        SDLActivity.nativeLowMemory();
+        component.onTrimMemory(level);
     }
 
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
-        Log.v(TAG, "onConfigurationChanged()");
         super.onConfigurationChanged(newConfig);
-
-        if (SDLActivity.mBrokenLibraries) {
-           return;
-        }
-
-        if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) {
-            mCurrentLocale = newConfig.locale;
-            SDLActivity.onNativeLocaleChanged();
-        }
-
-        switch (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) {
-        case Configuration.UI_MODE_NIGHT_NO:
-            SDLActivity.onNativeDarkModeChanged(false);
-            break;
-        case Configuration.UI_MODE_NIGHT_YES:
-            SDLActivity.onNativeDarkModeChanged(true);
-            break;
-        }
+        component.onConfigurationChanged(newConfig);
     }
 
     @Override
     protected void onDestroy() {
-        Log.v(TAG, "onDestroy()");
-
-        if (mHIDDeviceManager != null) {
-            HIDDeviceManager.release(mHIDDeviceManager);
-            mHIDDeviceManager = null;
-        }
-
-        SDLAudioManager.release(this);
-
-        if (SDLActivity.mBrokenLibraries) {
-           super.onDestroy();
-           return;
-        }
-
-        if (SDLActivity.mSDLThread != null) {
-
-            // Send Quit event to "SDLThread" thread
-            SDLActivity.nativeSendQuit();
-
-            // Wait for "SDLThread" thread to end
-            try {
-                // Use a timeout because:
-                // C SDLmain() thread might have started (mSDLThread.start() called)
-                // while the SDL_Init() might not have been called yet,
-                // and so the previous QUIT event will be discarded by SDL_Init() and app is running, not exiting.
-                SDLActivity.mSDLThread.join(1000);
-            } catch(Exception e) {
-                Log.v(TAG, "Problem stopping SDLThread: " + e);
-            }
-        }
-
-        SDLActivity.nativeQuit();
-
         super.onDestroy();
+        component.onDestroy();
     }
 
     @Override
     public void onBackPressed() {
-        // Check if we want to block the back button in case of mouse right click.
-        //
-        // If we do, the normal hardware back button will no longer work and people have to use home,
-        // but the mouse right click will work.
-        //
-        boolean trapBack = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_TRAP_BACK_BUTTON", false);
-        if (trapBack) {
-            // Exit and let the mouse handler handle this button (if appropriate)
-            return;
-        }
-
-        // Default system back button behavior.
-        if (!isFinishing()) {
+        if (component.onBackPressed()) {
             super.onBackPressed();
         }
     }
@@ -734,1448 +92,30 @@ public void onBackPressed() {
     @Override
     protected void onActivityResult(int requestCode, int resul

(Patch may be truncated, please check the link at the top of this post.)