Browse Source

Add support for using an Android Service to host the Godot engine
- Provide a `GodotService` Android service implementation which can be used to host an instance of the Godot engine
- Provide a `RemoteGodotFragment` Android fragment implementation which provides the view and logic to wrap connection to a `GodotService` instance

Fredia Huya-Kouadio 8 months ago
parent
commit
dc589e239c
22 changed files with 1118 additions and 400 deletions
  1. 3 1
      platform/android/java/app/build.gradle
  2. 0 2
      platform/android/java/app/src/com/godot/game/GodotApp.java
  3. 2 6
      platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt
  4. 11 0
      platform/android/java/lib/res/layout/remote_godot_fragment_layout.xml
  5. 144 213
      platform/android/java/lib/src/org/godotengine/godot/Godot.kt
  6. 10 8
      platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
  7. 3 32
      platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
  8. 2 4
      platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
  9. 17 1
      platform/android/java/lib/src/org/godotengine/godot/GodotHost.java
  10. 105 41
      platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
  11. 1 1
      platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
  12. 0 56
      platform/android/java/lib/src/org/godotengine/godot/GodotService.kt
  13. 2 4
      platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
  14. 24 8
      platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java
  15. 4 4
      platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java
  16. 427 0
      platform/android/java/lib/src/org/godotengine/godot/service/GodotService.kt
  17. 348 0
      platform/android/java/lib/src/org/godotengine/godot/service/RemoteGodotFragment.kt
  18. 1 1
      platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt
  19. 2 2
      platform/android/java_godot_lib_jni.cpp
  20. 1 1
      platform/android/java_godot_lib_jni.h
  21. 9 12
      platform/android/java_godot_wrapper.cpp
  22. 2 3
      platform/android/java_godot_wrapper.h

+ 3 - 1
platform/android/java/app/build.gradle

@@ -208,7 +208,9 @@ android {
     flavorDimensions 'edition'
 
     productFlavors {
-        standard {}
+        standard {
+            getIsDefault().set(true)
+        }
         mono {}
     }
 

+ 0 - 2
platform/android/java/app/src/com/godot/game/GodotApp.java

@@ -37,8 +37,6 @@ import android.util.Log;
 
 import androidx.core.splashscreen.SplashScreen;
 
-import com.godot.game.BuildConfig;
-
 /**
  * Template activity for Godot Android builds.
  * Feel free to extend and modify this class for your custom logic.

+ 2 - 6
platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt

@@ -33,6 +33,7 @@ package org.godotengine.editor
 import android.Manifest
 import android.util.Log
 import androidx.annotation.CallSuper
+import org.godotengine.godot.Godot
 import org.godotengine.godot.GodotLib
 import org.godotengine.godot.utils.GameMenuUtils
 import org.godotengine.godot.utils.PermissionsUtil
@@ -69,12 +70,7 @@ abstract class BaseGodotGame: GodotEditor() {
 					.putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, intent.getBundleExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD))
 
 				Log.d(TAG, "Relaunching XR project using ${editorWindowInfo.windowClassName} with parameters ${launchingArgs.contentToString()}")
-				val godot = godot
-				if (godot != null) {
-					godot.destroyAndKillProcess {
-						ProcessPhoenix.triggerRebirth(this, relaunchIntent)
-					}
-				} else {
+				Godot.getInstance(applicationContext).destroyAndKillProcess {
 					ProcessPhoenix.triggerRebirth(this, relaunchIntent)
 				}
 				return

+ 11 - 0
platform/android/java/lib/res/layout/remote_godot_fragment_layout.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent">
+
+	<SurfaceView
+		android:id="@+id/remote_godot_window_surface"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent" />
+
+</FrameLayout>

+ 144 - 213
platform/android/java/lib/src/org/godotengine/godot/Godot.kt

@@ -63,7 +63,6 @@ import org.godotengine.godot.plugin.AndroidRuntimePlugin
 import org.godotengine.godot.plugin.GodotPlugin
 import org.godotengine.godot.plugin.GodotPluginRegistry
 import org.godotengine.godot.tts.GodotTTS
-import org.godotengine.godot.utils.CommandLineFileParser
 import org.godotengine.godot.utils.DialogUtils
 import org.godotengine.godot.utils.GodotNetUtils
 import org.godotengine.godot.utils.PermissionsUtil
@@ -89,54 +88,51 @@ import java.util.concurrent.atomic.AtomicReference
  * Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its
  * lifecycle methods are properly invoked.
  */
-class Godot(private val context: Context) {
+class Godot private constructor(val context: Context) {
 
-	internal companion object {
+	companion object {
 		private val TAG = Godot::class.java.simpleName
 
+		@Volatile private var INSTANCE: Godot? = null
+
+		@JvmStatic
+		fun getInstance(context: Context): Godot {
+			return INSTANCE ?: synchronized(this) {
+				INSTANCE ?: Godot(context.applicationContext).also { INSTANCE = it }
+			}
+		}
+
 		// Supported build flavors
-		const val EDITOR_FLAVOR = "editor"
-		const val TEMPLATE_FLAVOR = "template"
+		private const val EDITOR_FLAVOR = "editor"
+		private const val TEMPLATE_FLAVOR = "template"
 
 		/**
 		 * @return true if this is an editor build, false if this is a template build
 		 */
-		fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
+		internal fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
 	}
 
-	private val mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
-	private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
-	private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
-
-	private val pluginRegistry: GodotPluginRegistry by lazy {
-		GodotPluginRegistry.getPluginRegistry()
-	}
+	private val mSensorManager: SensorManager? by lazy { context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager }
+	private val mClipboard: ClipboardManager? by lazy { context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager }
+	private val vibratorService: Vibrator? by lazy { context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator }
+	private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() }
 
 	private val accelerometerEnabled = AtomicBoolean(false)
-	private val mAccelerometer: Sensor? by lazy {
-		mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
-	}
+	private val mAccelerometer: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) }
 
 	private val gravityEnabled = AtomicBoolean(false)
-	private val mGravity: Sensor? by lazy {
-		mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
-	}
+	private val mGravity: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_GRAVITY) }
 
 	private val magnetometerEnabled = AtomicBoolean(false)
-	private val mMagnetometer: Sensor? by lazy {
-		mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
-	}
+	private val mMagnetometer: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) }
 
 	private val gyroscopeEnabled = AtomicBoolean(false)
-	private val mGyroscope: Sensor? by lazy {
-		mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
-	}
+	private val mGyroscope: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE) }
 
 	val tts = GodotTTS(context)
 	val directoryAccessHandler = DirectoryAccessHandler(context)
 	val fileAccessHandler = FileAccessHandler(context)
 	val netUtils = GodotNetUtils(context)
-	private val commandLineFileParser = CommandLineFileParser()
 	private val godotInputHandler = GodotInputHandler(context, this)
 
 	/**
@@ -144,11 +140,6 @@ class Godot(private val context: Context) {
 	 */
 	private val runOnTerminate = AtomicReference<Runnable>()
 
-	/**
-	 * Tracks whether [onCreate] was completed successfully.
-	 */
-	private var initializationStarted = false
-
 	/**
 	 * Tracks whether [GodotLib.initialize] was completed successfully.
 	 */
@@ -176,17 +167,15 @@ class Godot(private val context: Context) {
 	 */
 	private val godotMainLoopStarted = AtomicBoolean(false)
 
-	var io: GodotIO? = null
+	val io = GodotIO(this)
 
 	private var commandLine : MutableList<String> = ArrayList<String>()
 	private var xrMode = XRMode.REGULAR
-	private var expansionPackPath: String = ""
-	private var useApkExpansion = false
 	private val useImmersive = AtomicBoolean(false)
 	private var useDebugOpengl = false
 	private var darkMode = false
 
-	private var containerLayout: FrameLayout? = null
+	internal var containerLayout: FrameLayout? = null
 	var renderView: GodotRenderView? = null
 
 	/**
@@ -197,52 +186,45 @@ class Godot(private val context: Context) {
 	/**
 	 * Returns true if the engine has been initialized, false otherwise.
 	 */
-	fun isInitialized() = initializationStarted && isNativeInitialized() && renderViewInitialized
+	fun isInitialized() = primaryHost != null && isNativeInitialized() && renderViewInitialized
 
 	/**
 	 * Provides access to the primary host [Activity]
 	 */
 	fun getActivity() = primaryHost?.activity
-	private fun requireActivity() = getActivity() ?: throw IllegalStateException("Host activity must be non-null")
 
 	/**
 	 * Start initialization of the Godot engine.
 	 *
-	 * This must be followed by [onInitNativeLayer] and [onInitRenderView] in that order to complete
-	 * initialization of the engine.
+	 * This must be followed by [onInitRenderView] to complete initialization of the engine.
+	 *
+	 * @return false if initialization of the native layer fails, true otherwise.
 	 *
 	 * @throws IllegalArgumentException exception if the specified expansion pack (if any)
 	 * is invalid.
 	 */
-	fun onCreate(primaryHost: GodotHost) {
-		if (this.primaryHost != null || initializationStarted) {
-			Log.d(TAG, "OnCreate already invoked")
-			return
+	fun initEngine(commandLineParams: List<String>, hostPlugins: Set<GodotPlugin>): Boolean {
+		if (isNativeInitialized()) {
+			Log.d(TAG, "Engine already initialized")
+			return true
 		}
 
-		Log.v(TAG, "OnCreate: $primaryHost")
+		Log.v(TAG, "InitEngine with params: $commandLineParams")
 
 		darkMode = context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
 
-		beginBenchmarkMeasure("Startup", "Godot::onCreate")
+		beginBenchmarkMeasure("Startup", "Godot::initEngine")
 		try {
-			this.primaryHost = primaryHost
-			val activity = requireActivity()
-			val window = activity.window
-			window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
-
 			Log.v(TAG, "Initializing Godot plugin registry")
 			val runtimePlugins = mutableSetOf<GodotPlugin>(AndroidRuntimePlugin(this))
-			runtimePlugins.addAll(primaryHost.getHostPlugins(this))
+			runtimePlugins.addAll(hostPlugins)
 			GodotPluginRegistry.initializePluginRegistry(this, runtimePlugins)
-			if (io == null) {
-				io = GodotIO(activity)
-			}
 
 			// check for apk expansion API
-			commandLine = getCommandLine()
+			commandLine.addAll(commandLineParams)
 			var mainPackMd5: String? = null
 			var mainPackKey: String? = null
+			var useApkExpansion = false
 			val newArgs: MutableList<String> = ArrayList()
 			var i = 0
 			while (i < commandLine.size) {
@@ -263,7 +245,7 @@ class Godot(private val context: Context) {
 					i++
 				} else if (hasExtra && commandLine[i] == "--apk_expansion_key") {
 					mainPackKey = commandLine[i + 1]
-					val prefs = activity.getSharedPreferences(
+					val prefs = context.getSharedPreferences(
 							"app_data_keys",
 							Context.MODE_PRIVATE
 					)
@@ -288,15 +270,17 @@ class Godot(private val context: Context) {
 				}
 				i++
 			}
+
+			var expansionPackPath = ""
 			commandLine = if (newArgs.isEmpty()) { mutableListOf() } else { newArgs }
 			if (useApkExpansion && mainPackMd5 != null && mainPackKey != null) {
 				// Build the full path to the app's expansion files
 				try {
 					expansionPackPath = Helpers.getSaveFilePath(context)
-					expansionPackPath += "/main." + activity.packageManager.getPackageInfo(
-							activity.packageName,
+					expansionPackPath += "/main." + context.packageManager.getPackageInfo(
+							context.packageName,
 							0
-					).versionCode + "." + activity.packageName + ".obb"
+					).versionCode + "." + context.packageName + ".obb"
 				} catch (e: java.lang.Exception) {
 					Log.e(TAG, "Unable to build full path to the app's expansion files", e)
 				}
@@ -317,15 +301,35 @@ class Godot(private val context: Context) {
 				}
 			}
 
-			initializationStarted = true
-		} catch (e: java.lang.Exception) {
-			// Clear the primary host and rethrow
-			this.primaryHost = null
-			initializationStarted = false
-			throw e
+			if (expansionPackPath.isNotEmpty()) {
+				commandLine.add("--main-pack")
+				commandLine.add(expansionPackPath)
+			}
+			if (!nativeLayerInitializeCompleted) {
+				nativeLayerInitializeCompleted = GodotLib.initialize(
+					this,
+					context.assets,
+					io,
+					netUtils,
+					directoryAccessHandler,
+					fileAccessHandler,
+					useApkExpansion,
+				)
+				Log.v(TAG, "Godot native layer initialization completed: $nativeLayerInitializeCompleted")
+			}
+
+			if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) {
+				nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts)
+				if (!nativeLayerSetupCompleted) {
+					throw IllegalStateException("Unable to setup the Godot engine! Aborting...")
+				} else {
+					Log.v(TAG, "Godot native layer setup completed")
+				}
+			}
 		} finally {
-			endBenchmarkMeasure("Startup", "Godot::onCreate")
+			endBenchmarkMeasure("Startup", "Godot::initEngine")
 		}
+		return isNativeInitialized()
 	}
 
 	/**
@@ -368,7 +372,7 @@ class Godot(private val context: Context) {
 	 */
 	@Keep
 	private fun nativeEnableImmersiveMode(enabled: Boolean) {
-		runOnUiThread {
+		runOnHostThread {
 			enableImmersiveMode(enabled)
 		}
 	}
@@ -376,103 +380,51 @@ class Godot(private val context: Context) {
 	@Keep
 	fun isInImmersiveMode() = useImmersive.get()
 
-	/**
-	 * Initializes the native layer of the Godot engine.
-	 *
-	 * This must be preceded by [onCreate] and followed by [onInitRenderView] to complete
-	 * initialization of the engine.
-	 *
-	 * @return false if initialization of the native layer fails, true otherwise.
-	 *
-	 * @throws IllegalStateException if [onCreate] has not been called.
-	 */
-	fun onInitNativeLayer(host: GodotHost): Boolean {
-		if (!initializationStarted) {
-			throw IllegalStateException("OnCreate must be invoked successfully prior to initializing the native layer")
-		}
-		if (isNativeInitialized()) {
-			Log.d(TAG, "OnInitNativeLayer already invoked")
-			return true
-		}
-		if (host != primaryHost) {
-			Log.e(TAG, "Native initialization is only supported for the primary host")
-			return false
-		}
-
-		Log.v(TAG, "OnInitNativeLayer: $host")
-
-		beginBenchmarkMeasure("Startup", "Godot::onInitNativeLayer")
-		try {
-			if (expansionPackPath.isNotEmpty()) {
-				commandLine.add("--main-pack")
-				commandLine.add(expansionPackPath)
-			}
-			val activity = requireActivity()
-			if (!nativeLayerInitializeCompleted) {
-				nativeLayerInitializeCompleted = GodotLib.initialize(
-					activity,
-					this,
-					activity.assets,
-					io,
-					netUtils,
-					directoryAccessHandler,
-					fileAccessHandler,
-					useApkExpansion,
-				)
-				Log.v(TAG, "Godot native layer initialization completed: $nativeLayerInitializeCompleted")
-			}
-
-			if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) {
-				nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts)
-				if (!nativeLayerSetupCompleted) {
-					throw IllegalStateException("Unable to setup the Godot engine! Aborting...")
-				} else {
-					Log.v(TAG, "Godot native layer setup completed")
-				}
-			}
-		} finally {
-			endBenchmarkMeasure("Startup", "Godot::onInitNativeLayer")
-		}
-		return isNativeInitialized()
-	}
-
 	/**
 	 * Used to complete initialization of the view used by the engine for rendering.
 	 *
-	 * This must be preceded by [onCreate] and [onInitNativeLayer] in that order to properly
-	 * initialize the engine.
+	 * This must be preceded by [initEngine] to properly initialize the engine.
 	 *
 	 * @param host The [GodotHost] that's initializing the render views
 	 * @param providedContainerLayout Optional argument; if provided, this is reused to host the Godot's render views
 	 *
 	 * @return A [FrameLayout] instance containing Godot's render views if initialization is successful, null otherwise.
 	 *
-	 * @throws IllegalStateException if [onInitNativeLayer] has not been called
+	 * @throws IllegalStateException if [initEngine] has not been called
 	 */
 	@JvmOverloads
-	fun onInitRenderView(host: GodotHost, providedContainerLayout: FrameLayout = FrameLayout(host.activity)): FrameLayout? {
+	fun onInitRenderView(host: GodotHost, providedContainerLayout: FrameLayout = FrameLayout(context)): FrameLayout? {
 		if (!isNativeInitialized()) {
-			throw IllegalStateException("onInitNativeLayer() must be invoked successfully prior to initializing the render view")
+			throw IllegalStateException("initEngine(...) must be invoked successfully prior to initializing the render view")
 		}
 
-		Log.v(TAG, "OnInitRenderView: $host")
-
 		beginBenchmarkMeasure("Startup", "Godot::onInitRenderView")
+		Log.v(TAG, "OnInitRenderView: $host")
 		try {
-			val activity: Activity = host.activity
+			this.primaryHost = host
+			getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
+
+			if (containerLayout != null) {
+				assert(renderViewInitialized)
+				return containerLayout
+			}
+
 			containerLayout = providedContainerLayout
 			containerLayout?.removeAllViews()
-			containerLayout?.layoutParams = ViewGroup.LayoutParams(
+			val layoutParams = containerLayout?.layoutParams ?: ViewGroup.LayoutParams(
 					ViewGroup.LayoutParams.MATCH_PARENT,
 					ViewGroup.LayoutParams.MATCH_PARENT
 			)
+			layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
+			layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+			containerLayout?.layoutParams = layoutParams
 
 			// GodotEditText layout
-			val editText = GodotEditText(activity)
+			val editText = GodotEditText(context)
 			editText.layoutParams =
 					ViewGroup.LayoutParams(
 							ViewGroup.LayoutParams.MATCH_PARENT,
-							activity.resources.getDimension(R.dimen.text_edit_height).toInt()
+							context.resources.getDimension(R.dimen.text_edit_height).toInt()
 					)
 			// Prevent GodotEditText from showing on splash screen on devices with Android 14 or newer.
 			editText.setBackgroundColor(Color.TRANSPARENT)
@@ -484,25 +436,22 @@ class Godot(private val context: Context) {
 				!isProjectManagerHint() && !isEditorHint() && java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/per_pixel_transparency/allowed"))
 			Log.d(TAG, "Render view should be transparent: $shouldBeTransparent")
 			renderView = if (usesVulkan()) {
-				if (meetsVulkanRequirements(activity.packageManager)) {
-					GodotVulkanRenderView(host, this, godotInputHandler, shouldBeTransparent)
+				if (meetsVulkanRequirements(context.packageManager)) {
+					GodotVulkanRenderView(this, godotInputHandler, shouldBeTransparent)
 				} else if (canFallbackToOpenGL()) {
 					// Fallback to OpenGl.
-					GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
+					GodotGLRenderView(this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
 				} else {
-					throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message))
+					throw IllegalStateException(context.getString(R.string.error_missing_vulkan_requirements_message))
 				}
 
 			} else {
 				// Fallback to OpenGl.
-				GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
-			}
-
-			if (host == primaryHost) {
-				renderView?.startRenderer()
+				GodotGLRenderView(this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
 			}
 
 			renderView?.let {
+				it.startRenderer()
 				containerLayout?.addView(
 					it.view,
 					ViewGroup.LayoutParams(
@@ -513,20 +462,21 @@ class Godot(private val context: Context) {
 			}
 
 			editText.setView(renderView)
-			io?.setEdit(editText)
+			io.setEdit(editText)
 
+			val activity = host.activity
 			// Listeners for keyboard height.
-			val decorView = activity.window.decorView
+			val topView = activity?.window?.decorView ?: providedContainerLayout
 			// Report the height of virtual keyboard as it changes during the animation.
-			ViewCompat.setWindowInsetsAnimationCallback(decorView, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
+			ViewCompat.setWindowInsetsAnimationCallback(topView, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
 				var startBottom = 0
 				var endBottom = 0
 				override fun onPrepare(animation: WindowInsetsAnimationCompat) {
-					startBottom = ViewCompat.getRootWindowInsets(decorView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
+					startBottom = ViewCompat.getRootWindowInsets(topView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
 				}
 
 				override fun onStart(animation: WindowInsetsAnimationCompat, bounds: WindowInsetsAnimationCompat.BoundsCompat): WindowInsetsAnimationCompat.BoundsCompat {
-					endBottom = ViewCompat.getRootWindowInsets(decorView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
+					endBottom = ViewCompat.getRootWindowInsets(topView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
 					return bounds
 				}
 
@@ -553,23 +503,21 @@ class Godot(private val context: Context) {
 				override fun onEnd(animation: WindowInsetsAnimationCompat) {}
 			})
 
-			if (host == primaryHost) {
-				renderView?.queueOnRenderThread {
-					for (plugin in pluginRegistry.allPlugins) {
-						plugin.onRegisterPluginWithGodotNative()
-					}
-					setKeepScreenOn(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")))
+			renderView?.queueOnRenderThread {
+				for (plugin in pluginRegistry.allPlugins) {
+					plugin.onRegisterPluginWithGodotNative()
 				}
+				setKeepScreenOn(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")))
+			}
 
-				// Include the returned non-null views in the Godot view hierarchy.
-				for (plugin in pluginRegistry.allPlugins) {
-					val pluginView = plugin.onMainCreate(activity)
-					if (pluginView != null) {
-						if (plugin.shouldBeOnTop()) {
-							containerLayout?.addView(pluginView)
-						} else {
-							containerLayout?.addView(pluginView, 0)
-						}
+			// Include the returned non-null views in the Godot view hierarchy.
+			for (plugin in pluginRegistry.allPlugins) {
+				val pluginView = plugin.onMainCreate(activity)
+				if (pluginView != null) {
+					if (plugin.shouldBeOnTop()) {
+						containerLayout?.addView(pluginView)
+					} else {
+						containerLayout?.addView(pluginView, 0)
 					}
 				}
 			}
@@ -615,16 +563,16 @@ class Godot(private val context: Context) {
 		}
 
 		if (accelerometerEnabled.get() && mAccelerometer != null) {
-			mSensorManager.registerListener(godotInputHandler, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
+			mSensorManager?.registerListener(godotInputHandler, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
 		}
 		if (gravityEnabled.get() && mGravity != null) {
-			mSensorManager.registerListener(godotInputHandler, mGravity, SensorManager.SENSOR_DELAY_GAME)
+			mSensorManager?.registerListener(godotInputHandler, mGravity, SensorManager.SENSOR_DELAY_GAME)
 		}
 		if (magnetometerEnabled.get() && mMagnetometer != null) {
-			mSensorManager.registerListener(godotInputHandler, mMagnetometer, SensorManager.SENSOR_DELAY_GAME)
+			mSensorManager?.registerListener(godotInputHandler, mMagnetometer, SensorManager.SENSOR_DELAY_GAME)
 		}
 		if (gyroscopeEnabled.get() && mGyroscope != null) {
-			mSensorManager.registerListener(godotInputHandler, mGyroscope, SensorManager.SENSOR_DELAY_GAME)
+			mSensorManager?.registerListener(godotInputHandler, mGyroscope, SensorManager.SENSOR_DELAY_GAME)
 		}
 	}
 
@@ -636,7 +584,7 @@ class Godot(private val context: Context) {
 		}
 
 		renderView?.onActivityPaused()
-		mSensorManager.unregisterListener(godotInputHandler)
+		mSensorManager?.unregisterListener(godotInputHandler)
 		for (plugin in pluginRegistry.allPlugins) {
 			plugin.onMainPause()
 		}
@@ -652,16 +600,17 @@ class Godot(private val context: Context) {
 	}
 
 	fun onDestroy(primaryHost: GodotHost) {
-		Log.v(TAG, "OnDestroy: $primaryHost")
 		if (this.primaryHost != primaryHost) {
 			return
 		}
+		Log.v(TAG, "OnDestroy: $primaryHost")
 
 		for (plugin in pluginRegistry.allPlugins) {
 			plugin.onMainDestroy()
 		}
 
 		renderView?.onActivityDestroyed()
+		this.primaryHost = null
 	}
 
 	/**
@@ -721,7 +670,7 @@ class Godot(private val context: Context) {
 		val overrideVolumeButtons = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/override_volume_buttons"))
 		val scrollDeadzoneDisabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/disable_scroll_deadzone"))
 
-		runOnUiThread {
+		runOnHostThread {
 			renderView?.inputHandler?.apply {
 				enableLongPress(longPressEnabled)
 				enablePanningAndScalingGestures(panScaleEnabled)
@@ -753,7 +702,7 @@ class Godot(private val context: Context) {
 		gyroscopeEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gyroscope")))
 		magnetometerEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_magnetometer")))
 
-		runOnUiThread {
+		runOnHostThread {
 			registerSensorsIfNeeded()
 			enableImmersiveMode(useImmersive.get(), true)
 		}
@@ -782,15 +731,15 @@ class Godot(private val context: Context) {
 		@StringRes titleResId: Int,
 		okCallback: Runnable?
 	) {
-		val res: Resources = getActivity()?.resources ?: return
+		val res: Resources = context.resources ?: return
 		alert(res.getString(messageResId), res.getString(titleResId), okCallback)
 	}
 
 	@JvmOverloads
 	@Keep
 	fun alert(message: String, title: String, okCallback: Runnable? = null) {
-		val activity: Activity = getActivity() ?: return
-		runOnUiThread {
+		val activity = getActivity() ?: return
+		runOnHostThread {
 			val builder = AlertDialog.Builder(activity)
 			builder.setMessage(message).setTitle(title)
 			builder.setPositiveButton(
@@ -814,14 +763,10 @@ class Godot(private val context: Context) {
 	}
 
 	/**
-	 * Runs the specified action on the UI thread.
-	 * If the current thread is the UI thread, then the action is executed immediately.
-	 * If the current thread is not the UI thread, the action is posted to the event queue
-	 * of the UI thread.
+	 * Runs the specified action on the host thread.
 	 */
-	fun runOnUiThread(action: Runnable) {
-		val activity: Activity = getActivity() ?: return
-		activity.runOnUiThread(action)
+	fun runOnHostThread(action: Runnable) {
+		primaryHost?.runOnHostThread(action)
 	}
 
 	/**
@@ -838,7 +783,7 @@ class Godot(private val context: Context) {
 		var renderingDevice = rendererInfo[0]
 		var rendererSource = "ProjectSettings"
 		var renderer = rendererInfo[1]
-		val cmdline = getCommandLine()
+		val cmdline = commandLine
 		var index = cmdline.indexOf("--rendering-method")
 		if (index > -1 && cmdline.size > index + 1) {
 			rendererSource = "CommandLine"
@@ -880,7 +825,7 @@ class Godot(private val context: Context) {
 	}
 
 	private fun setKeepScreenOn(enabled: Boolean) {
-		runOnUiThread {
+		runOnHostThread {
 			if (enabled) {
 				getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
 			} else {
@@ -905,19 +850,21 @@ class Godot(private val context: Context) {
 		return darkMode
 	}
 
+	@Keep
 	fun hasClipboard(): Boolean {
-		return mClipboard.hasPrimaryClip()
+		return mClipboard?.hasPrimaryClip() == true
 	}
 
+	@Keep
 	fun getClipboard(): String {
-		val clipData = mClipboard.primaryClip ?: return ""
+		val clipData = mClipboard?.primaryClip ?: return ""
 		val text = clipData.getItemAt(0).text ?: return ""
 		return text.toString()
 	}
 
+	@Keep
 	fun setClipboard(text: String?) {
-		val clip = ClipData.newPlainText("myLabel", text)
-		mClipboard.setPrimaryClip(clip)
+		mClipboard?.setPrimaryClip(ClipData.newPlainText("myLabel", text))
 	}
 
 	@Keep
@@ -971,8 +918,7 @@ class Godot(private val context: Context) {
 	@JvmOverloads
 	fun destroyAndKillProcess(destroyRunnable: Runnable? = null) {
 		val host = primaryHost
-		val activity = host?.activity
-		if (host == null || activity == null) {
+		if (host == null) {
 			// Run the destroyRunnable right away as we are about to force quit.
 			destroyRunnable?.run()
 
@@ -984,7 +930,7 @@ class Godot(private val context: Context) {
 		// Store the destroyRunnable so it can be run when the engine is terminating
 		runOnTerminate.set(destroyRunnable)
 
-		runOnUiThread {
+		runOnHostThread {
 			onDestroy(host)
 		}
 	}
@@ -1019,14 +965,14 @@ class Godot(private val context: Context) {
 			try {
 				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 					if (amplitude <= -1) {
-						vibratorService.vibrate(
+						vibratorService?.vibrate(
 							VibrationEffect.createOneShot(
 								durationMs.toLong(),
 								VibrationEffect.DEFAULT_AMPLITUDE
 							)
 						)
 					} else {
-						vibratorService.vibrate(
+						vibratorService?.vibrate(
 							VibrationEffect.createOneShot(
 								durationMs.toLong(),
 								amplitude
@@ -1035,7 +981,7 @@ class Godot(private val context: Context) {
 					}
 				} else {
 					// deprecated in API 26
-					vibratorService.vibrate(durationMs.toLong())
+					vibratorService?.vibrate(durationMs.toLong())
 				}
 			} catch (e: SecurityException) {
 				Log.w(TAG, "SecurityException: VIBRATE permission not found. Make sure it is declared in the manifest or enabled in the export preset.")
@@ -1043,21 +989,6 @@ class Godot(private val context: Context) {
 		}
 	}
 
-	private fun getCommandLine(): MutableList<String> {
-		val commandLine = try {
-			commandLineFileParser.parseCommandLine(requireActivity().assets.open("_cl_"))
-		} catch (ignored: Exception) {
-			mutableListOf()
-		}
-
-		val hostCommandLine = primaryHost?.commandLine
-		if (!hostCommandLine.isNullOrEmpty()) {
-			commandLine.addAll(hostCommandLine)
-		}
-
-		return commandLine
-	}
-
 	/**
 	 * Used by the native code (java_godot_wrapper.h) to access the input fallback mapping.
 	 * @return The input fallback mapping for the current XR mode.
@@ -1077,7 +1008,7 @@ class Godot(private val context: Context) {
 	}
 
 	fun getGrantedPermissions(): Array<String?>? {
-		return PermissionsUtil.getGrantedPermissions(getActivity())
+		return PermissionsUtil.getGrantedPermissions(context)
 	}
 
 	/**

+ 10 - 8
platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt

@@ -39,6 +39,7 @@ import android.util.Log
 import androidx.annotation.CallSuper
 import androidx.annotation.LayoutRes
 import androidx.fragment.app.FragmentActivity
+import org.godotengine.godot.utils.CommandLineFileParser
 import org.godotengine.godot.utils.PermissionsUtil
 import org.godotengine.godot.utils.ProcessPhoenix
 
@@ -73,6 +74,13 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
 
 	@CallSuper
 	override fun onCreate(savedInstanceState: Bundle?) {
+		val assetsCommandLine = try {
+			CommandLineFileParser.parseCommandLine(assets.open("_cl_"))
+		} catch (ignored: Exception) {
+			mutableListOf()
+		}
+		commandLineParams.addAll(assetsCommandLine)
+
 		val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
 		Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}")
 		commandLineParams.addAll(params ?: emptyArray())
@@ -107,12 +115,7 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
 
 	protected fun triggerRebirth(bundle: Bundle?, intent: Intent) {
 		// Launch a new activity
-		val godot = godot
-		if (godot != null) {
-			godot.destroyAndKillProcess {
-				ProcessPhoenix.triggerRebirth(this, bundle, intent)
-			}
-		} else {
+		Godot.getInstance(applicationContext).destroyAndKillProcess {
 			ProcessPhoenix.triggerRebirth(this, bundle, intent)
 		}
 	}
@@ -159,8 +162,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
 		intent = newIntent
 
 		handleStartIntent(newIntent, false)
-
-		godotFragment?.onNewIntent(newIntent)
 	}
 
 	private fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
@@ -215,5 +216,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
 		return GodotFragment()
 	}
 
+	@CallSuper
 	override fun getCommandLine(): MutableList<String> = commandLineParams
 }

+ 3 - 32
platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java

@@ -89,26 +89,14 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
 	private View mCellMessage;
 
 	private Button mPauseButton;
-	private Button mWiFiSettingsButton;
 
 	private FrameLayout godotContainerLayout;
-	private boolean mStatePaused;
 	private int mState;
 
 	@Nullable
 	private GodotHost parentHost;
 	private Godot godot;
 
-	static private Intent mCurrentIntent;
-
-	public void onNewIntent(Intent intent) {
-		mCurrentIntent = intent;
-	}
-
-	static public Intent getCurrentIntent() {
-		return mCurrentIntent;
-	}
-
 	private void setState(int newState) {
 		if (mState != newState) {
 			mState = newState;
@@ -117,16 +105,10 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
 	}
 
 	private void setButtonPausedState(boolean paused) {
-		mStatePaused = paused;
 		int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause;
 		mPauseButton.setText(stringResourceID);
 	}
 
-	public interface ResultCallback {
-		void callback(int requestCode, int resultCode, Intent data);
-	}
-	public ResultCallback resultCallback;
-
 	@Override
 	public Godot getGodot() {
 		return godot;
@@ -159,11 +141,6 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
 	@Override
 	public void onActivityResult(int requestCode, int resultCode, Intent data) {
 		super.onActivityResult(requestCode, resultCode, data);
-		if (resultCallback != null) {
-			resultCallback.callback(requestCode, resultCode, data);
-			resultCallback = null;
-		}
-
 		godot.onActivityResult(requestCode, resultCode, data);
 	}
 
@@ -185,14 +162,11 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
 		BenchmarkUtils.beginBenchmarkMeasure("Startup", "GodotFragment::onCreate");
 		super.onCreate(icicle);
 
-		final Activity activity = getActivity();
-		mCurrentIntent = activity.getIntent();
-
 		if (parentHost != null) {
 			godot = parentHost.getGodot();
 		}
 		if (godot == null) {
-			godot = new Godot(requireContext());
+			godot = Godot.getInstance(requireContext());
 		}
 		performEngineInitialization();
 		BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate");
@@ -200,10 +174,8 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
 
 	private void performEngineInitialization() {
 		try {
-			godot.onCreate(this);
-
-			if (!godot.onInitNativeLayer(this)) {
-				throw new IllegalStateException("Unable to initialize engine native layer");
+			if (!godot.initEngine(getCommandLine(), getHostPlugins(godot))) {
+				throw new IllegalStateException("Unable to initialize Godot engine");
 			}
 
 			godotContainerLayout = godot.onInitRenderView(this);
@@ -257,7 +229,6 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
 			mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard);
 			mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular);
 			mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton);
-			mWiFiSettingsButton = (Button)downloadingExpansionView.findViewById(R.id.wifiSettingsButton);
 
 			return downloadingExpansionView;
 		}

+ 2 - 4
platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java

@@ -76,16 +76,14 @@ import java.io.InputStream;
  *   bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
  */
 class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
-	private final GodotHost host;
 	private final Godot godot;
 	private final GodotInputHandler inputHandler;
 	private final GodotRenderer godotRenderer;
 	private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
 
-	public GodotGLRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl, boolean shouldBeTranslucent) {
-		super(host.getActivity());
+	public GodotGLRenderView(Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl, boolean shouldBeTranslucent) {
+		super(godot.getContext());
 
-		this.host = host;
 		this.godot = godot;
 		this.inputHandler = inputHandler;
 		this.godotRenderer = new GodotRenderer();

+ 17 - 1
platform/android/java/lib/src/org/godotengine/godot/GodotHost.java

@@ -36,6 +36,7 @@ import org.godotengine.godot.plugin.GodotPlugin;
 import android.app.Activity;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import java.util.Collections;
 import java.util.List;
@@ -96,8 +97,9 @@ public interface GodotHost {
 	}
 
 	/**
-	 * Provide access to the Activity hosting the {@link Godot} engine.
+	 * Provide access to the Activity hosting the {@link Godot} engine if any.
 	 */
+	@Nullable
 	Activity getActivity();
 
 	/**
@@ -150,4 +152,18 @@ public interface GodotHost {
 	 * Invoked on the render thread when an editor workspace has been selected.
 	 */
 	default void onEditorWorkspaceSelected(String workspace) {}
+
+	/**
+	 * Runs the specified action on a host provided thread.
+	 */
+	default void runOnHostThread(Runnable action) {
+		if (action == null) {
+			return;
+		}
+
+		Activity activity = getActivity();
+		if (activity != null) {
+			activity.runOnUiThread(action);
+		}
+	}
 }

+ 105 - 41
platform/android/java/lib/src/org/godotengine/godot/GodotIO.java

@@ -48,6 +48,7 @@ import android.util.Log;
 import android.view.Display;
 import android.view.DisplayCutout;
 import android.view.Surface;
+import android.view.View;
 import android.view.WindowInsets;
 import android.view.WindowManager;
 
@@ -62,7 +63,8 @@ import java.util.Locale;
 public class GodotIO {
 	private static final String TAG = GodotIO.class.getSimpleName();
 
-	private final Activity activity;
+	private final Godot godot;
+
 	private final String uniqueId;
 	GodotEditText edit;
 
@@ -74,9 +76,9 @@ public class GodotIO {
 	final int SCREEN_SENSOR_PORTRAIT = 5;
 	final int SCREEN_SENSOR = 6;
 
-	GodotIO(Activity p_activity) {
-		activity = p_activity;
-		String androidId = Settings.Secure.getString(activity.getContentResolver(),
+	GodotIO(Godot godot) {
+		this.godot = godot;
+		String androidId = Settings.Secure.getString(godot.getContext().getContentResolver(),
 				Settings.Secure.ANDROID_ID);
 		if (androidId == null) {
 			androidId = "";
@@ -85,12 +87,22 @@ public class GodotIO {
 		uniqueId = androidId;
 	}
 
+	private Context getContext() {
+		Context context = godot.getActivity();
+		if (context == null) {
+			context = godot.getContext();
+		}
+		return context;
+	}
+
 	/////////////////////////
 	// MISCELLANEOUS OS IO
 	/////////////////////////
 
 	public int openURI(String uriString) {
 		try {
+			Context context = getContext();
+
 			Uri dataUri;
 			String dataType = "";
 			boolean grantReadUriPermission = false;
@@ -104,14 +116,14 @@ public class GodotIO {
 				}
 
 				File targetFile = new File(filePath);
-				dataUri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", targetFile);
-				dataType = activity.getContentResolver().getType(dataUri);
+				dataUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", targetFile);
+				dataType = context.getContentResolver().getType(dataUri);
 			} else {
 				dataUri = Uri.parse(uriString);
 			}
 
 			Intent intent = new Intent();
-			intent.setAction(Intent.ACTION_VIEW);
+			intent.setAction(Intent.ACTION_VIEW).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 			if (TextUtils.isEmpty(dataType)) {
 				intent.setData(dataUri);
 			} else {
@@ -121,7 +133,7 @@ public class GodotIO {
 				intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 			}
 
-			activity.startActivity(intent);
+			context.startActivity(intent);
 			return Error.OK.toNativeValue();
 		} catch (Exception e) {
 			Log.e(TAG, "Unable to open uri " + uriString, e);
@@ -130,7 +142,7 @@ public class GodotIO {
 	}
 
 	public String getCacheDir() {
-		return activity.getCacheDir().getAbsolutePath();
+		return getContext().getCacheDir().getAbsolutePath();
 	}
 
 	public String getTempDir() {
@@ -146,7 +158,7 @@ public class GodotIO {
 	}
 
 	public String getDataDir() {
-		return activity.getFilesDir().getAbsolutePath();
+		return getContext().getFilesDir().getAbsolutePath();
 	}
 
 	public String getLocale() {
@@ -158,14 +170,14 @@ public class GodotIO {
 	}
 
 	public int getScreenDPI() {
-		return activity.getResources().getDisplayMetrics().densityDpi;
+		return getContext().getResources().getDisplayMetrics().densityDpi;
 	}
 
 	/**
 	 * Returns bucketized density values.
 	 */
 	public float getScaledDensity() {
-		int densityDpi = activity.getResources().getDisplayMetrics().densityDpi;
+		int densityDpi = getContext().getResources().getDisplayMetrics().densityDpi;
 		float selectedScaledDensity;
 		if (densityDpi >= DisplayMetrics.DENSITY_XXXHIGH) {
 			selectedScaledDensity = 4.0f;
@@ -184,7 +196,15 @@ public class GodotIO {
 	}
 
 	public double getScreenRefreshRate(double fallback) {
-		Display display = activity.getWindowManager().getDefaultDisplay();
+		Activity activity = godot.getActivity();
+
+		Display display = null;
+		if (activity != null) {
+			display = activity.getWindowManager().getDefaultDisplay();
+		} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+			display = godot.getContext().getDisplay();
+		}
+
 		if (display != null) {
 			return display.getRefreshRate();
 		}
@@ -193,30 +213,57 @@ public class GodotIO {
 
 	public int[] getDisplaySafeArea() {
 		Rect rect = new Rect();
-		activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
-
-		int[] result = { rect.left, rect.top, rect.right, rect.bottom };
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
-			WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets();
-			DisplayCutout cutout = insets.getDisplayCutout();
-			if (cutout != null) {
-				int insetLeft = cutout.getSafeInsetLeft();
-				int insetTop = cutout.getSafeInsetTop();
-				result[0] = insetLeft;
-				result[1] = insetTop;
-				result[2] -= insetLeft + cutout.getSafeInsetRight();
-				result[3] -= insetTop + cutout.getSafeInsetBottom();
+		int[] result = new int[4];
+
+		View topView = null;
+		if (godot.getActivity() != null) {
+			topView = godot.getActivity().getWindow().getDecorView();
+		} else if (godot.getRenderView() != null) {
+			topView = godot.getRenderView().getView();
+		}
+
+		if (topView != null) {
+			topView.getWindowVisibleDisplayFrame(rect);
+			result[0] = rect.left;
+			result[1] = rect.top;
+			result[2] = rect.right;
+			result[3] = rect.bottom;
+
+			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+				WindowInsets insets = topView.getRootWindowInsets();
+				DisplayCutout cutout = insets.getDisplayCutout();
+				if (cutout != null) {
+					int insetLeft = cutout.getSafeInsetLeft();
+					int insetTop = cutout.getSafeInsetTop();
+					result[0] = insetLeft;
+					result[1] = insetTop;
+					result[2] -= insetLeft + cutout.getSafeInsetRight();
+					result[3] -= insetTop + cutout.getSafeInsetBottom();
+				}
 			}
 		}
 		return result;
 	}
 
 	public int[] getDisplayCutouts() {
-		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
+		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
 			return new int[0];
-		DisplayCutout cutout = activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
-		if (cutout == null)
+		}
+
+		View topView = null;
+		if (godot.getActivity() != null) {
+			topView = godot.getActivity().getWindow().getDecorView();
+		} else if (godot.getRenderView() != null) {
+			topView = godot.getRenderView().getView();
+		}
+
+		if (topView == null) {
+			return new int[0];
+		}
+		DisplayCutout cutout = topView.getRootWindowInsets().getDisplayCutout();
+		if (cutout == null) {
 			return new int[0];
+		}
 		List<Rect> rects = cutout.getBoundingRects();
 		int cutouts = rects.size();
 		int[] result = new int[cutouts * 4];
@@ -242,9 +289,6 @@ public class GodotIO {
 		if (edit != null) {
 			edit.showKeyboard(p_existing_text, GodotEditText.VirtualKeyboardType.values()[p_type], p_max_input_length, p_cursor_start, p_cursor_end);
 		}
-
-		//InputMethodManager inputMgr = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
-		//inputMgr.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
 	}
 
 	public void hideKeyboard() {
@@ -253,6 +297,11 @@ public class GodotIO {
 	}
 
 	public void setScreenOrientation(int p_orientation) {
+		final Activity activity = godot.getActivity();
+		if (activity == null) {
+			return;
+		}
+
 		switch (p_orientation) {
 			case SCREEN_LANDSCAPE: {
 				activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
@@ -279,6 +328,11 @@ public class GodotIO {
 	}
 
 	public int getScreenOrientation() {
+		final Activity activity = godot.getActivity();
+		if (activity == null) {
+			return -1;
+		}
+
 		int orientation = activity.getRequestedOrientation();
 		switch (orientation) {
 			case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:
@@ -310,14 +364,24 @@ public class GodotIO {
 	}
 
 	public int getDisplayRotation() {
-		WindowManager windowManager = (WindowManager)activity.getSystemService(Context.WINDOW_SERVICE);
-		int rotation = windowManager.getDefaultDisplay().getRotation();
-		if (rotation == Surface.ROTATION_90) {
-			return 90;
-		} else if (rotation == Surface.ROTATION_180) {
-			return 180;
-		} else if (rotation == Surface.ROTATION_270) {
-			return 270;
+		Activity activity = godot.getActivity();
+
+		Display display = null;
+		if (activity != null) {
+			display = activity.getWindowManager().getDefaultDisplay();
+		} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+			display = godot.getContext().getDisplay();
+		}
+
+		if (display != null) {
+			int rotation = display.getRotation();
+			if (rotation == Surface.ROTATION_90) {
+				return 90;
+			} else if (rotation == Surface.ROTATION_180) {
+				return 180;
+			} else if (rotation == Surface.ROTATION_270) {
+				return 270;
+			}
 		}
 		return 0;
 	}
@@ -382,7 +446,7 @@ public class GodotIO {
 				return Environment.getExternalStoragePublicDirectory(what).getAbsolutePath();
 			}
 		} else {
-			return activity.getExternalFilesDir(what).getAbsolutePath();
+			return getContext().getExternalFilesDir(what).getAbsolutePath();
 		}
 	}
 

+ 1 - 1
platform/android/java/lib/src/org/godotengine/godot/GodotLib.java

@@ -55,7 +55,7 @@ public class GodotLib {
 	/**
 	 * Invoked on the main thread to initialize Godot native layer.
 	 */
-	public static native boolean initialize(Activity activity,
+	public static native boolean initialize(
 			Godot p_instance,
 			AssetManager p_asset_manager,
 			GodotIO godotIO,

+ 0 - 56
platform/android/java/lib/src/org/godotengine/godot/GodotService.kt

@@ -1,56 +0,0 @@
-package org.godotengine.godot
-
-import android.app.Service
-import android.content.Intent
-import android.os.Binder
-import android.os.IBinder
-import android.util.Log
-
-/**
- * Godot service responsible for hosting the Godot engine instance.
- *
- * Note: Still in development, so it's made private and inaccessible until completed.
- */
-private class GodotService : Service() {
-
-	companion object {
-		private val TAG = GodotService::class.java.simpleName
-	}
-
-	private var boundIntent: Intent? = null
-	private val godot by lazy {
-		Godot(applicationContext)
-	}
-
-	override fun onCreate() {
-		super.onCreate()
-	}
-
-	override fun onDestroy() {
-		super.onDestroy()
-	}
-
-	override fun onBind(intent: Intent?): IBinder? {
-		if (boundIntent != null) {
-			Log.d(TAG, "GodotService already bound")
-			return null
-		}
-
-		boundIntent = intent
-		return GodotHandle(godot)
-	}
-
-	override fun onRebind(intent: Intent?) {
-		super.onRebind(intent)
-	}
-
-	override fun onUnbind(intent: Intent?): Boolean {
-		return super.onUnbind(intent)
-	}
-
-	override fun onTaskRemoved(rootIntent: Intent?) {
-		super.onTaskRemoved(rootIntent)
-	}
-
-	class GodotHandle(val godot: Godot) : Binder()
-}

+ 2 - 4
platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java

@@ -51,16 +51,14 @@ import androidx.annotation.Keep;
 import java.io.InputStream;
 
 class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
-	private final GodotHost host;
 	private final Godot godot;
 	private final GodotInputHandler mInputHandler;
 	private final VkRenderer mRenderer;
 	private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
 
-	public GodotVulkanRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler, boolean shouldBeTranslucent) {
-		super(host.getActivity());
+	public GodotVulkanRenderView(Godot godot, GodotInputHandler inputHandler, boolean shouldBeTranslucent) {
+		super(godot.getContext());
 
-		this.host = host;
 		this.godot = godot;
 		mInputHandler = inputHandler;
 		mRenderer = new VkRenderer();

+ 24 - 8
platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java

@@ -34,6 +34,7 @@ import org.godotengine.godot.BuildConfig;
 import org.godotengine.godot.Godot;
 
 import android.app.Activity;
+import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 import android.util.Log;
@@ -46,10 +47,8 @@ import androidx.annotation.Nullable;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -109,6 +108,13 @@ public abstract class GodotPlugin {
 		return godot.getActivity();
 	}
 
+	/**
+	 * Provides access to the {@link Context}.
+	 */
+	protected Context getContext() {
+		return godot.getContext();
+	}
+
 	/**
 	 * Register the plugin with Godot native code.
 	 * <p>
@@ -179,7 +185,7 @@ public abstract class GodotPlugin {
 	 * @return the plugin's view to be included; null if no views should be included.
 	 */
 	@Nullable
-	public View onMainCreate(Activity activity) {
+	public View onMainCreate(@Nullable Activity activity) {
 		return null;
 	}
 
@@ -323,14 +329,24 @@ public abstract class GodotPlugin {
 	}
 
 	/**
-	 * Runs the specified action on the UI thread. If the current thread is the UI
-	 * thread, then the action is executed immediately. If the current thread is
-	 * not the UI thread, the action is posted to the event queue of the UI thread.
+	 * Runs the specified action on the host thread.
+	 *
+	 * @param action the action to run on the host thread
 	 *
-	 * @param action the action to run on the UI thread
+	 * @deprecated Use the {@link GodotPlugin#runOnHostThread} instead.
 	 */
+	@Deprecated
 	protected void runOnUiThread(Runnable action) {
-		godot.runOnUiThread(action);
+		runOnHostThread(action);
+	}
+
+	/**
+	 * Runs the specified action on the host thread.
+	 *
+	 * @param action the action to run on the host thread
+	 */
+	protected void runOnHostThread(Runnable action) {
+		godot.runOnHostThread(action);
 	}
 
 	/**

+ 4 - 4
platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java

@@ -32,7 +32,7 @@ package org.godotengine.godot.plugin;
 
 import org.godotengine.godot.Godot;
 
-import android.app.Activity;
+import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
@@ -134,10 +134,10 @@ public final class GodotPluginRegistry {
 
 		// Register the manifest plugins
 		try {
-			final Activity activity = godot.getActivity();
-			ApplicationInfo appInfo = activity
+			final Context context = godot.getContext();
+			ApplicationInfo appInfo = context
 											  .getPackageManager()
-											  .getApplicationInfo(activity.getPackageName(),
+											  .getApplicationInfo(context.getPackageName(),
 													  PackageManager.GET_META_DATA);
 			Bundle metaData = appInfo.metaData;
 			if (metaData == null || metaData.isEmpty()) {

+ 427 - 0
platform/android/java/lib/src/org/godotengine/godot/service/GodotService.kt

@@ -0,0 +1,427 @@
+/**************************************************************************/
+/*  GodotService.kt                                                       */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+package org.godotengine.godot.service
+
+import android.app.Service
+import android.content.Intent
+import android.hardware.display.DisplayManager
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.IBinder
+import android.os.Message
+import android.os.Messenger
+import android.os.Process
+import android.os.RemoteException
+import android.text.TextUtils
+import android.util.Log
+import android.view.SurfaceControlViewHost
+import android.widget.FrameLayout
+import androidx.annotation.CallSuper
+import androidx.annotation.RequiresApi
+import androidx.core.os.bundleOf
+import org.godotengine.godot.Godot
+import org.godotengine.godot.GodotHost
+import org.godotengine.godot.R
+import java.lang.ref.WeakReference
+
+/**
+ * Specialized [Service] implementation able to host a Godot engine instance.
+ *
+ * When used remotely (from another process), this component lacks access to an [android.app.Activity]
+ * instance, and as such it does not have full access to the set of Godot UI capabilities.
+ *
+ * Limitations: As of version 4.5, use of vulkan + swappy causes [GodotService] to crash as swappy requires an Activity
+ * context. So [GodotService] should be used with OpenGL or with Vulkan with swappy disabled.
+ */
+open class GodotService : Service() {
+
+	companion object {
+		private val TAG = GodotService::class.java.simpleName
+
+		const val EXTRA_MSG_PAYLOAD = "extraMsgPayload"
+
+		// Keys to store / retrieve msg payloads
+		const val KEY_COMMAND_LINE_PARAMETERS = "commandLineParameters"
+		const val KEY_HOST_TOKEN = "hostToken"
+		const val KEY_DISPLAY_ID = "displayId"
+		const val KEY_WIDTH = "width"
+		const val KEY_HEIGHT = "height"
+		const val KEY_SURFACE_PACKAGE = "surfacePackage"
+		const val KEY_ENGINE_STATUS = "engineStatus"
+		const val KEY_ENGINE_ERROR = "engineError"
+
+		// Set of commands from the client to the service
+		const val MSG_INIT_ENGINE = 0
+		const val MSG_START_ENGINE = MSG_INIT_ENGINE + 1
+		const val MSG_STOP_ENGINE = MSG_START_ENGINE + 1
+		const val MSG_DESTROY_ENGINE = MSG_STOP_ENGINE + 1
+
+		@RequiresApi(Build.VERSION_CODES.R)
+		const val MSG_WRAP_ENGINE_WITH_SCVH = MSG_DESTROY_ENGINE + 1
+
+		// Set of commands from the service to the client
+		const val MSG_ENGINE_ERROR = 100
+		const val MSG_ENGINE_STATUS_UPDATE = 101
+		const val MSG_ENGINE_RESTART_REQUESTED = 102
+	}
+
+	enum class EngineStatus {
+		INITIALIZED,
+		SCVH_CREATED,
+		STARTED,
+		STOPPED,
+		DESTROYED,
+	}
+
+	enum class EngineError {
+		ALREADY_BOUND,
+		INIT_FAILED,
+		SCVH_CREATION_FAILED,
+	}
+
+	/**
+	 * Used to subscribe to engine's updates.
+	 */
+	private class RemoteListener(val handlerRef: WeakReference<IncomingHandler>, val replyTo: Messenger) {
+		fun onEngineError(error: EngineError, extras: Bundle? = null) {
+			try {
+				replyTo.send(Message.obtain().apply {
+					what = MSG_ENGINE_ERROR
+					data.putString(KEY_ENGINE_ERROR, error.name)
+					if (extras != null && !extras.isEmpty) {
+						data.putAll(extras)
+					}
+				})
+			} catch (e: RemoteException) {
+				Log.e(TAG, "Unable to send engine error", e)
+			}
+		}
+
+		fun onEngineStatusUpdate(status: EngineStatus, extras: Bundle? = null) {
+			try {
+				replyTo.send(Message.obtain().apply {
+					what = MSG_ENGINE_STATUS_UPDATE
+					data.putString(KEY_ENGINE_STATUS, status.name)
+					if (extras != null && !extras.isEmpty) {
+						data.putAll(extras)
+					}
+				})
+			} catch (e: RemoteException) {
+				Log.e(TAG, "Unable to send engine status update", e)
+			}
+
+			if (status == EngineStatus.DESTROYED) {
+				val handler = handlerRef.get() ?: return
+				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && handler.viewHost != null) {
+					Log.d(TAG, "Releasing SurfaceControlViewHost")
+					handler.viewHost?.release()
+					handler.viewHost = null
+				}
+			}
+		}
+
+		fun onEngineRestartRequested() {
+			try {
+				replyTo.send(Message.obtain(null, MSG_ENGINE_RESTART_REQUESTED))
+			} catch (e: RemoteException) {
+				Log.w(TAG, "Unable to send restart request", e)
+			}
+		}
+	}
+
+	/**
+	 * Handler of incoming messages from remote clients.
+	 */
+	private class IncomingHandler(private val serviceRef: WeakReference<GodotService>) : Handler() {
+
+		var viewHost: SurfaceControlViewHost? = null
+
+		override fun handleMessage(msg: Message) {
+			val service = serviceRef.get() ?: return
+
+			Log.d(TAG, "HandleMessage: $msg")
+
+			if (msg.replyTo == null) {
+				// Messages for this handler must have a valid 'replyTo' field
+				super.handleMessage(msg)
+				return
+			}
+
+			try {
+				val serviceListener = service.listener
+				if (serviceListener == null) {
+					service.listener = RemoteListener(WeakReference(this), msg.replyTo)
+				} else if (serviceListener.replyTo != msg.replyTo) {
+					Log.e(TAG, "Engine is already bound to another client")
+					msg.replyTo.send(Message.obtain().apply {
+						what = MSG_ENGINE_ERROR
+						data.putString(KEY_ENGINE_ERROR, EngineError.ALREADY_BOUND.name)
+					})
+					return
+				}
+
+				when (msg.what) {
+					MSG_INIT_ENGINE -> service.initEngine(msg.data.getStringArray(KEY_COMMAND_LINE_PARAMETERS))
+
+					MSG_START_ENGINE -> service.startEngine()
+
+					MSG_STOP_ENGINE -> service.stopEngine()
+
+					MSG_DESTROY_ENGINE -> service.destroyEngine()
+
+					MSG_WRAP_ENGINE_WITH_SCVH -> {
+						if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+							Log.e(TAG, "SDK version is less than the minimum required (${Build.VERSION_CODES.R})")
+							service.listener?.onEngineError(EngineError.SCVH_CREATION_FAILED)
+							return
+						}
+
+						var currentViewHost = viewHost
+						if (currentViewHost != null) {
+							Log.i(TAG, "Attached Godot engine to SurfaceControlViewHost")
+							service.listener?.onEngineStatusUpdate(
+								EngineStatus.SCVH_CREATED,
+								bundleOf(KEY_SURFACE_PACKAGE to currentViewHost.surfacePackage)
+							)
+							return
+						}
+
+						val msgData = msg.data
+						if (msgData.isEmpty) {
+							Log.e(TAG, "Invalid message data from binding client.. Aborting")
+							service.listener?.onEngineError(EngineError.SCVH_CREATION_FAILED)
+							return
+						}
+
+						val godotContainerLayout = service.godot.containerLayout
+						if (godotContainerLayout == null) {
+							Log.e(TAG, "Invalid godot layout.. Aborting")
+							service.listener?.onEngineError(EngineError.SCVH_CREATION_FAILED)
+							return
+						}
+
+						val hostToken = msgData.getBinder(KEY_HOST_TOKEN)
+						val width = msgData.getInt(KEY_WIDTH)
+						val height = msgData.getInt(KEY_HEIGHT)
+						val displayId = msgData.getInt(KEY_DISPLAY_ID)
+						val display = service.getSystemService(DisplayManager::class.java)
+							.getDisplay(displayId)
+
+						Log.d(TAG, "Setting up SurfaceControlViewHost")
+						currentViewHost = SurfaceControlViewHost(service, display, hostToken).apply {
+							setView(godotContainerLayout, width, height)
+
+							Log.i(TAG, "Attached Godot engine to SurfaceControlViewHost")
+							service.listener?.onEngineStatusUpdate(
+								EngineStatus.SCVH_CREATED,
+								bundleOf(KEY_SURFACE_PACKAGE to surfacePackage)
+							)
+						}
+						viewHost = currentViewHost
+					}
+
+					else -> super.handleMessage(msg)
+				}
+			} catch (e: RemoteException) {
+				Log.e(TAG, "Unable to handle message", e)
+			}
+		}
+	}
+
+	private inner class GodotServiceHost : GodotHost {
+		override fun getActivity() = null
+		override fun getGodot() = [email protected]
+		override fun getCommandLine() = commandLineParams
+
+		override fun runOnHostThread(action: Runnable) {
+			if (Thread.currentThread() != handler.looper.thread) {
+				handler.post(action)
+			} else {
+				action.run()
+			}
+		}
+
+		override fun onGodotForceQuit(instance: Godot) {
+			if (instance === godot) {
+				Log.d(TAG, "Force quitting Godot service")
+				forceQuitService()
+			}
+		}
+
+		override fun onGodotRestartRequested(instance: Godot) {
+			if (instance === godot) {
+				Log.d(TAG, "Restarting Godot service")
+				listener?.onEngineRestartRequested()
+			}
+		}
+	}
+
+	private val commandLineParams = ArrayList<String>()
+	private val handler = IncomingHandler(WeakReference(this))
+	private val messenger = Messenger(handler)
+	private val godotHost = GodotServiceHost()
+
+	private val godot: Godot by lazy { Godot.getInstance(applicationContext) }
+	private var listener: RemoteListener? = null
+
+	override fun onCreate() {
+		Log.d(TAG, "OnCreate")
+		super.onCreate()
+	}
+
+	override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+		// Dispatch the start payload to the incoming handler
+		Log.d(TAG, "Processing start command $intent")
+		val msg = intent?.getParcelableExtra<Message>(EXTRA_MSG_PAYLOAD)
+		if (msg != null) {
+			handler.sendMessage(msg)
+		}
+		return START_NOT_STICKY
+	}
+
+	@CallSuper
+	protected open fun updateCommandLineParams(args: List<String>) {
+		// Update the list of command line params with the new args
+		commandLineParams.clear()
+		if (args.isNotEmpty()) {
+			commandLineParams.addAll(args)
+		}
+	}
+
+	private fun performEngineInitialization(): Boolean {
+		Log.d(TAG, "Performing engine initialization")
+		try {
+			// Initialize the Godot instance
+			if (!godot.initEngine(godotHost.commandLine, godotHost.getHostPlugins(godot))) {
+				throw IllegalStateException("Unable to initialize Godot engine layer")
+			}
+
+			if (godot.onInitRenderView(godotHost) == null) {
+				throw IllegalStateException("Unable to initialize engine render view")
+			}
+			return true
+		} catch (e: IllegalStateException) {
+			Log.e(TAG, "Engine initialization failed", e)
+			val errorMessage = if (TextUtils.isEmpty(e.message)
+			) {
+				getString(R.string.error_engine_setup_message)
+			} else {
+				e.message!!
+			}
+			godot.alert(errorMessage, getString(R.string.text_error_title)) { godot.destroyAndKillProcess() }
+			return false
+		}
+	}
+
+	override fun onDestroy() {
+		Log.d(TAG, "OnDestroy")
+		super.onDestroy()
+		destroyEngine()
+	}
+
+	private fun forceQuitService() {
+		Log.d(TAG, "Force quitting service")
+		stopSelf()
+		Process.killProcess(Process.myPid())
+		Runtime.getRuntime().exit(0)
+	}
+
+	override fun onBind(intent: Intent?): IBinder? = messenger.binder
+
+	override fun onUnbind(intent: Intent?): Boolean {
+		stopEngine()
+		return false
+	}
+
+	private fun initEngine(args: Array<String>?): FrameLayout? {
+		if (!godot.isInitialized()) {
+			if (!args.isNullOrEmpty()) {
+				updateCommandLineParams(args.asList())
+			}
+
+			if (!performEngineInitialization()) {
+				Log.e(TAG, "Unable to initialize Godot engine")
+				return null
+			} else {
+				Log.i(TAG, "Engine initialization complete!")
+			}
+		}
+		val godotContainerLayout = godot.containerLayout
+		if (godotContainerLayout == null) {
+			listener?.onEngineError(EngineError.INIT_FAILED)
+		} else {
+			Log.i(TAG, "Initialized Godot engine")
+			listener?.onEngineStatusUpdate(EngineStatus.INITIALIZED)
+		}
+
+		return godotContainerLayout
+	}
+
+	private fun startEngine() {
+		if (!godot.isInitialized()) {
+			Log.e(TAG, "Attempting to start uninitialized Godot engine instance")
+			return
+		}
+
+		Log.d(TAG, "Starting Godot engine")
+		godot.onStart(godotHost)
+		godot.onResume(godotHost)
+
+		listener?.onEngineStatusUpdate(EngineStatus.STARTED)
+	}
+
+	private fun stopEngine() {
+		if (!godot.isInitialized()) {
+			Log.e(TAG, "Attempting to stop uninitialized Godot engine instance")
+			return
+		}
+
+		Log.d(TAG, "Stopping Godot engine")
+		godot.onPause(godotHost)
+		godot.onStop(godotHost)
+
+		listener?.onEngineStatusUpdate(EngineStatus.STOPPED)
+	}
+
+	private fun destroyEngine() {
+		if (!godot.isInitialized()) {
+			return
+		}
+
+		godot.onDestroy(godotHost)
+
+		listener?.onEngineStatusUpdate(EngineStatus.DESTROYED)
+		listener = null
+		forceQuitService()
+	}
+
+}

+ 348 - 0
platform/android/java/lib/src/org/godotengine/godot/service/RemoteGodotFragment.kt

@@ -0,0 +1,348 @@
+/**************************************************************************/
+/*  RemoteGodotFragment.kt                                                */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+package org.godotengine.godot.service
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.IBinder
+import android.os.Message
+import android.os.Messenger
+import android.os.RemoteException
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.SurfaceControlViewHost
+import android.view.SurfaceView
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.RequiresApi
+import androidx.fragment.app.Fragment
+import org.godotengine.godot.GodotHost
+import org.godotengine.godot.R
+import org.godotengine.godot.service.GodotService.EngineStatus.*
+import org.godotengine.godot.service.GodotService.EngineError.*
+import java.lang.ref.WeakReference
+
+/**
+ * Godot [Fragment] component showcasing how to drive rendering from another process using a [GodotService] instance.
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+class RemoteGodotFragment: Fragment() {
+
+	companion object {
+		internal val TAG = RemoteGodotFragment::class.java.simpleName
+	}
+
+	/**
+	 * Target we publish for receiving messages from the service.
+	 */
+	private val messengerForReply = Messenger(IncomingHandler(WeakReference<RemoteGodotFragment>(this)))
+
+	/**
+	 * Messenger for sending messages to the [GodotService] implementation.
+	 */
+	private var serviceMessenger: Messenger? = null
+
+	private var remoteSurface : SurfaceView? = null
+
+	private var engineInitialized = false
+	private var fragmentStarted = false
+	private var serviceBound = false
+	private var remoteGameArgs = arrayOf<String>()
+
+	private var godotHost : GodotHost? = null
+
+	private val serviceConnection = object : ServiceConnection {
+		override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+			Log.d(TAG, "Connected to service $name")
+			serviceMessenger = Messenger(service)
+
+			// Initialize the Godot engine
+			initGodotEngine()
+		}
+
+		override fun onServiceDisconnected(name: ComponentName?) {
+			Log.d(TAG, "Disconnected from service $name")
+			serviceMessenger = null
+		}
+	}
+
+	/**
+	 * Handler of incoming messages from [GodotService] implementations.
+	 */
+	private class IncomingHandler(private val fragmentRef: WeakReference<RemoteGodotFragment>) : Handler() {
+
+		override fun handleMessage(msg: Message) {
+			val fragment = fragmentRef.get() ?: return
+
+			try {
+				Log.d(TAG, "HandleMessage: $msg")
+
+				when (msg.what) {
+					GodotService.MSG_ENGINE_STATUS_UPDATE -> {
+						try {
+							val engineStatus = GodotService.EngineStatus.valueOf(
+								msg.data.getString(GodotService.KEY_ENGINE_STATUS, "")
+							)
+							Log.d(TAG, "Received engine status $engineStatus")
+
+							when (engineStatus) {
+								INITIALIZED -> {
+									Log.d(TAG, "Engine initialized!")
+
+									try {
+										Log.i(TAG, "Creating SurfaceControlViewHost...")
+										fragment.remoteSurface?.let {
+											fragment.serviceMessenger?.send(Message.obtain().apply {
+												what = GodotService.MSG_WRAP_ENGINE_WITH_SCVH
+												data.apply {
+													putBinder(GodotService.KEY_HOST_TOKEN, it.hostToken)
+													putInt(GodotService.KEY_DISPLAY_ID, it.display.displayId)
+													putInt(GodotService.KEY_WIDTH, it.width)
+													putInt(GodotService.KEY_HEIGHT, it.height)
+												}
+												replyTo = fragment.messengerForReply
+											})
+										}
+									} catch (e: RemoteException) {
+										Log.e(TAG, "Unable to set up SurfaceControlViewHost", e)
+									}
+								}
+
+								STARTED -> {
+									Log.d(TAG, "Engine started!")
+								}
+
+								STOPPED -> {
+									Log.d(TAG, "Engine stopped!")
+								}
+
+								DESTROYED -> {
+									Log.d(TAG, "Engine destroyed!")
+									fragment.engineInitialized = false
+								}
+
+								SCVH_CREATED -> {
+									Log.d(TAG, "SurfaceControlViewHost created!")
+
+									val surfacePackage = msg.data.getParcelable<SurfaceControlViewHost.SurfacePackage>(
+										GodotService.KEY_SURFACE_PACKAGE)
+									if (surfacePackage == null) {
+										Log.e(TAG, "Unable to retrieve surface package from GodotService")
+									} else {
+										fragment.remoteSurface?.setChildSurfacePackage(surfacePackage)
+										fragment.engineInitialized = true
+										fragment.startGodotEngine()
+									}
+								}
+							}
+						} catch (e: IllegalArgumentException) {
+							Log.e(TAG, "Unable to retrieve engine status update from $msg")
+						}
+					}
+
+					GodotService.MSG_ENGINE_ERROR -> {
+						try {
+							val engineError = GodotService.EngineError.valueOf(
+								msg.data.getString(GodotService.KEY_ENGINE_ERROR, "")
+							)
+							Log.d(TAG, "Received engine error $engineError")
+
+							when (engineError) {
+								ALREADY_BOUND -> {
+									// Engine is already connected to another client, unbind for now
+									fragment.stopRemoteGame(false)
+								}
+
+								INIT_FAILED -> {
+									Log.e(TAG, "Engine initialization failed")
+								}
+
+								SCVH_CREATION_FAILED -> {
+									Log.e(TAG, "SurfaceControlViewHost creation failed")
+								}
+							}
+						} catch (e: IllegalArgumentException) {
+							Log.e(TAG, "Unable to retrieve engine error from message $msg", e)
+						}
+					}
+
+					GodotService.MSG_ENGINE_RESTART_REQUESTED -> {
+						Log.d(TAG, "Engine restart requested")
+						// Validate the engine is actually running
+						if (!fragment.serviceBound || !fragment.engineInitialized) {
+							return
+						}
+
+						// Retrieve the current game args since stopping the engine will clear them out
+						val currentArgs = fragment.remoteGameArgs
+
+						// Stop the engine
+						fragment.stopRemoteGame()
+
+						// Restart the engine
+						fragment.startRemoteGame(currentArgs)
+					}
+
+					else -> super.handleMessage(msg)
+				}
+			} catch (e: RemoteException) {
+				Log.e(TAG, "Unable to handle message $msg", e)
+			}
+		}
+	}
+
+	override fun onAttach(context: Context) {
+		super.onAttach(context)
+		val parentActivity = activity
+		if (parentActivity is GodotHost) {
+			godotHost = parentActivity
+		} else {
+			val parentFragment = parentFragment
+			if (parentFragment is GodotHost) {
+				godotHost = parentFragment
+			}
+		}
+	}
+
+	override fun onDetach() {
+		super.onDetach()
+		godotHost = null
+	}
+
+	override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?): View? {
+		return inflater.inflate(R.layout.remote_godot_fragment_layout, container, false)
+	}
+
+	override fun onViewCreated(view: View, bundle: Bundle?) {
+		super.onViewCreated(view, bundle)
+		remoteSurface = view.findViewById(R.id.remote_godot_window_surface)
+		remoteSurface?.setZOrderOnTop(false)
+
+		initGodotEngine()
+	}
+
+	fun startRemoteGame(args: Array<String>) {
+		Log.d(TAG, "Starting remote game with args: ${args.contentToString()}")
+		remoteSurface?.setZOrderOnTop(true)
+		remoteGameArgs = args
+		context?.bindService(
+			Intent(context, GodotService::class.java),
+			serviceConnection,
+			Context.BIND_AUTO_CREATE
+		)
+		serviceBound = true
+	}
+
+	fun stopRemoteGame(destroyEngine: Boolean = true) {
+		Log.d(TAG, "Stopping remote game")
+		remoteSurface?.setZOrderOnTop(false)
+		remoteGameArgs = arrayOf()
+
+		if (serviceBound) {
+			if (destroyEngine) {
+				serviceMessenger?.send(Message.obtain().apply {
+					what = GodotService.MSG_DESTROY_ENGINE
+					replyTo = messengerForReply
+				})
+			}
+			context?.unbindService(serviceConnection)
+			serviceBound = false
+		}
+	}
+
+	private fun initGodotEngine() {
+		if (!serviceBound) {
+			return
+		}
+
+		try {
+			serviceMessenger?.send(Message.obtain().apply {
+				what = GodotService.MSG_INIT_ENGINE
+				data.apply {
+					putStringArray(GodotService.KEY_COMMAND_LINE_PARAMETERS, remoteGameArgs)
+				}
+				replyTo = messengerForReply
+			})
+		} catch (e: RemoteException) {
+			Log.e(TAG, "Unable to initialize Godot engine", e)
+		}
+	}
+
+	private fun startGodotEngine() {
+		if (!serviceBound || !engineInitialized || !fragmentStarted) {
+			return
+		}
+		try {
+			serviceMessenger?.send(Message.obtain().apply {
+				what = GodotService.MSG_START_ENGINE
+				replyTo = messengerForReply
+			})
+		} catch (e: RemoteException) {
+			Log.e(TAG, "Unable to start Godot engine", e)
+		}
+	}
+
+	private fun stopGodotEngine() {
+		if (!serviceBound || !engineInitialized || fragmentStarted) {
+			return
+		}
+		try {
+			serviceMessenger?.send(Message.obtain().apply {
+				what = GodotService.MSG_STOP_ENGINE
+				replyTo = messengerForReply
+			})
+		} catch (e: RemoteException) {
+			Log.e(TAG, "Unable to stop Godot engine", e)
+		}
+	}
+
+	override fun onStart() {
+		super.onStart()
+		fragmentStarted = true
+		startGodotEngine()
+	}
+
+	override fun onStop() {
+		super.onStop()
+		fragmentStarted = false
+		stopGodotEngine()
+	}
+
+	override fun onDestroy() {
+		stopRemoteGame()
+		super.onDestroy()
+	}
+}

+ 1 - 1
platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt

@@ -40,7 +40,7 @@ import java.util.ArrayList
  *
  * Returns a mutable list of command lines
  */
-internal class CommandLineFileParser {
+internal object CommandLineFileParser {
 	fun parseCommandLine(inputStream: InputStream): MutableList<String> {
 		return try {
 			val headerBytes = ByteArray(4)

+ 2 - 2
platform/android/java_godot_lib_jni.cpp

@@ -140,12 +140,12 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei
 	}
 }
 
-JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion) {
+JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion) {
 	JavaVM *jvm;
 	env->GetJavaVM(&jvm);
 
 	// create our wrapper classes
-	godot_java = new GodotJavaWrapper(env, p_activity, p_godot_instance);
+	godot_java = new GodotJavaWrapper(env, p_godot_instance);
 	godot_io_java = new GodotIOJavaWrapper(env, p_godot_io);
 
 	init_thread_jandroid(jvm, env);

+ 1 - 1
platform/android/java_godot_lib_jni.h

@@ -36,7 +36,7 @@
 // These functions can be called from within JAVA and are the means by which our JAVA implementation calls back into our C++ code.
 // See java/src/org/godotengine/godot/GodotLib.java for the JAVA side of this (yes that's why we have the long names)
 extern "C" {
-JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion);
+JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz);
 JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline, jobject p_godot_tts);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jobject p_surface, jint p_width, jint p_height);

+ 9 - 12
platform/android/java_godot_wrapper.cpp

@@ -37,9 +37,8 @@
 
 // TODO we could probably create a base class for this...
 
-GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance) {
+GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance) {
 	godot_instance = p_env->NewGlobalRef(p_godot_instance);
-	activity = p_env->NewGlobalRef(p_activity);
 
 	// get info about our Godot class so we can get pointers and stuff...
 	godot_class = p_env->FindClass("org/godotengine/godot/Godot");
@@ -49,13 +48,6 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 		// this is a pretty serious fail.. bail... pointers will stay 0
 		return;
 	}
-	activity_class = p_env->FindClass("android/app/Activity");
-	if (activity_class) {
-		activity_class = (jclass)p_env->NewGlobalRef(activity_class);
-	} else {
-		// this is a pretty serious fail.. bail... pointers will stay 0
-		return;
-	}
 
 	// get some Godot method pointers...
 	_restart = p_env->GetMethodID(godot_class, "restart", "()V");
@@ -94,6 +86,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 	_enable_immersive_mode = p_env->GetMethodID(godot_class, "nativeEnableImmersiveMode", "(Z)V");
 	_is_in_immersive_mode = p_env->GetMethodID(godot_class, "isInImmersiveMode", "()Z");
 	_on_editor_workspace_selected = p_env->GetMethodID(godot_class, "nativeOnEditorWorkspaceSelected", "(Ljava/lang/String;)V");
+	_get_activity = p_env->GetMethodID(godot_class, "getActivity", "()Landroid/app/Activity;");
 }
 
 GodotJavaWrapper::~GodotJavaWrapper() {
@@ -105,12 +98,16 @@ GodotJavaWrapper::~GodotJavaWrapper() {
 	ERR_FAIL_NULL(env);
 	env->DeleteGlobalRef(godot_instance);
 	env->DeleteGlobalRef(godot_class);
-	env->DeleteGlobalRef(activity);
-	env->DeleteGlobalRef(activity_class);
 }
 
 jobject GodotJavaWrapper::get_activity() {
-	return activity;
+	if (_get_activity) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_NULL_V(env, nullptr);
+		jobject activity = env->CallObjectMethod(godot_instance, _get_activity);
+		return activity;
+	}
+	return nullptr;
 }
 
 GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() {

+ 2 - 3
platform/android/java_godot_wrapper.h

@@ -42,9 +42,7 @@
 class GodotJavaWrapper {
 private:
 	jobject godot_instance;
-	jobject activity;
 	jclass godot_class;
-	jclass activity_class;
 
 	GodotJavaViewWrapper *godot_view = nullptr;
 
@@ -84,9 +82,10 @@ private:
 	jmethodID _enable_immersive_mode = nullptr;
 	jmethodID _is_in_immersive_mode = nullptr;
 	jmethodID _on_editor_workspace_selected = nullptr;
+	jmethodID _get_activity = nullptr;
 
 public:
-	GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
+	GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance);
 	~GodotJavaWrapper();
 
 	jobject get_activity();