From dc589e239c7b68fd4ea34df3394391f68f5f1b6a Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Mon, 3 Feb 2025 04:34:29 -0800 Subject: [PATCH] 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 --- platform/android/java/app/build.gradle | 4 +- .../java/app/src/com/godot/game/GodotApp.java | 2 - .../org/godotengine/editor/BaseGodotGame.kt | 8 +- .../layout/remote_godot_fragment_layout.xml | 11 + .../lib/src/org/godotengine/godot/Godot.kt | 359 ++++++--------- .../org/godotengine/godot/GodotActivity.kt | 18 +- .../org/godotengine/godot/GodotFragment.java | 35 +- .../godotengine/godot/GodotGLRenderView.java | 6 +- .../src/org/godotengine/godot/GodotHost.java | 18 +- .../src/org/godotengine/godot/GodotIO.java | 144 ++++-- .../src/org/godotengine/godot/GodotLib.java | 2 +- .../src/org/godotengine/godot/GodotService.kt | 56 --- .../godot/GodotVulkanRenderView.java | 6 +- .../godotengine/godot/plugin/GodotPlugin.java | 32 +- .../godot/plugin/GodotPluginRegistry.java | 8 +- .../godotengine/godot/service/GodotService.kt | 427 ++++++++++++++++++ .../godot/service/RemoteGodotFragment.kt | 348 ++++++++++++++ .../godot/utils/CommandLineFileParser.kt | 2 +- platform/android/java_godot_lib_jni.cpp | 4 +- platform/android/java_godot_lib_jni.h | 2 +- platform/android/java_godot_wrapper.cpp | 21 +- platform/android/java_godot_wrapper.h | 5 +- 22 files changed, 1118 insertions(+), 400 deletions(-) create mode 100644 platform/android/java/lib/res/layout/remote_godot_fragment_layout.xml delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/GodotService.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/service/GodotService.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/service/RemoteGodotFragment.kt diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index f479c43a649..a3a2b9a4ce7 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -208,7 +208,9 @@ android { flavorDimensions 'edition' productFlavors { - standard {} + standard { + getIsDefault().set(true) + } mono {} } diff --git a/platform/android/java/app/src/com/godot/game/GodotApp.java b/platform/android/java/app/src/com/godot/game/GodotApp.java index 9d4991e1200..254222485d8 100644 --- a/platform/android/java/app/src/com/godot/game/GodotApp.java +++ b/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. diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt index 3c3837bdc1c..59c209c662b 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt +++ b/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 diff --git a/platform/android/java/lib/res/layout/remote_godot_fragment_layout.xml b/platform/android/java/lib/res/layout/remote_godot_fragment_layout.xml new file mode 100644 index 00000000000..8c58cc9fe23 --- /dev/null +++ b/platform/android/java/lib/res/layout/remote_godot_fragment_layout.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index 8b4973d5771..e0813c511ae 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/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() - /** - * 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 = ArrayList() 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, hostPlugins: Set): 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(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 = 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"))) - } - - // Include the returned non-null views in the Godot view hierarchy. + renderView?.queueOnRenderThread { for (plugin in pluginRegistry.allPlugins) { - val pluginView = plugin.onMainCreate(activity) - if (pluginView != null) { - if (plugin.shouldBeOnTop()) { - containerLayout?.addView(pluginView) - } else { - containerLayout?.addView(pluginView, 0) - } + 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) } } } @@ -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 { - 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? { - return PermissionsUtil.getGrantedPermissions(getActivity()) + return PermissionsUtil.getGrantedPermissions(context) } /** diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt index a40653b1d23..fb7bd560a2f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt +++ b/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 = commandLineParams } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java index 358e65777a7..a32e7f8be95 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java +++ b/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; } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index bddea29b4d4..fdd0d544485 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/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 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(); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java index 9b44fe60769..466da4465a6 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java +++ b/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); + } + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index 503ed756845..1088c5b63e4 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/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 = new int[4]; - 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(); + 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 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(); } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index 5e599432d91..bbc7c1d5614 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/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, diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotService.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotService.kt deleted file mode 100644 index 795dc921c7f..00000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotService.kt +++ /dev/null @@ -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() -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index f9a552fd23d..6287065f114 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/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 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(); diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java index c975c29e969..8b0f785458b 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java +++ b/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. *

@@ -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 UI thread + * @param action the action to run on the host 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); } /** diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java index 8976dd65db6..7171e6772f0 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java +++ b/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()) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/service/GodotService.kt b/platform/android/java/lib/src/org/godotengine/godot/service/GodotService.kt new file mode 100644 index 00000000000..34b5e91fb81 --- /dev/null +++ b/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, 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) : 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() = this@GodotService.godot + 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() + 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(EXTRA_MSG_PAYLOAD) + if (msg != null) { + handler.sendMessage(msg) + } + return START_NOT_STICKY + } + + @CallSuper + protected open fun updateCommandLineParams(args: List) { + // 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?): 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() + } + +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/service/RemoteGodotFragment.kt b/platform/android/java/lib/src/org/godotengine/godot/service/RemoteGodotFragment.kt new file mode 100644 index 00000000000..8aa6df79b4a --- /dev/null +++ b/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(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() + + 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) : 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( + 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) { + 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() + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt index ce5c5b67143..d6878948fcb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt +++ b/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 { return try { val headerBytes = ByteArray(4) diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 0604f0fe712..d2102785c32 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/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); diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 7cdc22d7189..fe8c5c7593c 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/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); diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 1260786b6c5..dfd4849d8b5 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/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() { diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index e0fd9077fea..a7957b18073 100644 --- a/platform/android/java_godot_wrapper.h +++ b/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();