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();