1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-04 12:00:25 +00:00

Merge pull request #102866 from m4gr3d/implement_godot_service

Add support for using an Android Service to host the Godot engine
This commit is contained in:
Rémi Verschelde
2025-06-14 00:00:00 +02:00
22 changed files with 1118 additions and 400 deletions

View File

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

View File

@@ -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.

View File

@@ -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

View File

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

View File

@@ -63,7 +63,6 @@ import org.godotengine.godot.plugin.AndroidRuntimePlugin
import org.godotengine.godot.plugin.GodotPlugin
import org.godotengine.godot.plugin.GodotPluginRegistry
import org.godotengine.godot.tts.GodotTTS
import org.godotengine.godot.utils.CommandLineFileParser
import org.godotengine.godot.utils.DialogUtils
import org.godotengine.godot.utils.GodotNetUtils
import org.godotengine.godot.utils.PermissionsUtil
@@ -89,54 +88,51 @@ import java.util.concurrent.atomic.AtomicReference
* Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its
* lifecycle methods are properly invoked.
*/
class Godot(private val context: Context) {
class Godot private constructor(val context: Context) {
internal companion object {
companion object {
private val TAG = Godot::class.java.simpleName
@Volatile private var INSTANCE: Godot? = null
@JvmStatic
fun getInstance(context: Context): Godot {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Godot(context.applicationContext).also { INSTANCE = it }
}
}
// Supported build flavors
const val EDITOR_FLAVOR = "editor"
const val TEMPLATE_FLAVOR = "template"
private const val EDITOR_FLAVOR = "editor"
private const val TEMPLATE_FLAVOR = "template"
/**
* @return true if this is an editor build, false if this is a template build
*/
fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
internal fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
}
private val mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
private val pluginRegistry: GodotPluginRegistry by lazy {
GodotPluginRegistry.getPluginRegistry()
}
private val mSensorManager: SensorManager? by lazy { context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager }
private val mClipboard: ClipboardManager? by lazy { context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager }
private val vibratorService: Vibrator? by lazy { context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator }
private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() }
private val accelerometerEnabled = AtomicBoolean(false)
private val mAccelerometer: Sensor? by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
}
private val mAccelerometer: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) }
private val gravityEnabled = AtomicBoolean(false)
private val mGravity: Sensor? by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
}
private val mGravity: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_GRAVITY) }
private val magnetometerEnabled = AtomicBoolean(false)
private val mMagnetometer: Sensor? by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
}
private val mMagnetometer: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) }
private val gyroscopeEnabled = AtomicBoolean(false)
private val mGyroscope: Sensor? by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
}
private val mGyroscope: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE) }
val tts = GodotTTS(context)
val directoryAccessHandler = DirectoryAccessHandler(context)
val fileAccessHandler = FileAccessHandler(context)
val netUtils = GodotNetUtils(context)
private val commandLineFileParser = CommandLineFileParser()
private val godotInputHandler = GodotInputHandler(context, this)
/**
@@ -144,11 +140,6 @@ class Godot(private val context: Context) {
*/
private val runOnTerminate = AtomicReference<Runnable>()
/**
* Tracks whether [onCreate] was completed successfully.
*/
private var initializationStarted = false
/**
* Tracks whether [GodotLib.initialize] was completed successfully.
*/
@@ -176,17 +167,15 @@ class Godot(private val context: Context) {
*/
private val godotMainLoopStarted = AtomicBoolean(false)
var io: GodotIO? = null
val io = GodotIO(this)
private var commandLine : MutableList<String> = ArrayList<String>()
private var xrMode = XRMode.REGULAR
private var expansionPackPath: String = ""
private var useApkExpansion = false
private val useImmersive = AtomicBoolean(false)
private var useDebugOpengl = false
private var darkMode = false
private var containerLayout: FrameLayout? = null
internal var containerLayout: FrameLayout? = null
var renderView: GodotRenderView? = null
/**
@@ -197,52 +186,45 @@ class Godot(private val context: Context) {
/**
* Returns true if the engine has been initialized, false otherwise.
*/
fun isInitialized() = initializationStarted && isNativeInitialized() && renderViewInitialized
fun isInitialized() = primaryHost != null && isNativeInitialized() && renderViewInitialized
/**
* Provides access to the primary host [Activity]
*/
fun getActivity() = primaryHost?.activity
private fun requireActivity() = getActivity() ?: throw IllegalStateException("Host activity must be non-null")
/**
* Start initialization of the Godot engine.
*
* This must be followed by [onInitNativeLayer] and [onInitRenderView] in that order to complete
* initialization of the engine.
* This must be followed by [onInitRenderView] to complete initialization of the engine.
*
* @return false if initialization of the native layer fails, true otherwise.
*
* @throws IllegalArgumentException exception if the specified expansion pack (if any)
* is invalid.
*/
fun onCreate(primaryHost: GodotHost) {
if (this.primaryHost != null || initializationStarted) {
Log.d(TAG, "OnCreate already invoked")
return
fun initEngine(commandLineParams: List<String>, hostPlugins: Set<GodotPlugin>): Boolean {
if (isNativeInitialized()) {
Log.d(TAG, "Engine already initialized")
return true
}
Log.v(TAG, "OnCreate: $primaryHost")
Log.v(TAG, "InitEngine with params: $commandLineParams")
darkMode = context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
beginBenchmarkMeasure("Startup", "Godot::onCreate")
beginBenchmarkMeasure("Startup", "Godot::initEngine")
try {
this.primaryHost = primaryHost
val activity = requireActivity()
val window = activity.window
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
Log.v(TAG, "Initializing Godot plugin registry")
val runtimePlugins = mutableSetOf<GodotPlugin>(AndroidRuntimePlugin(this))
runtimePlugins.addAll(primaryHost.getHostPlugins(this))
runtimePlugins.addAll(hostPlugins)
GodotPluginRegistry.initializePluginRegistry(this, runtimePlugins)
if (io == null) {
io = GodotIO(activity)
}
// check for apk expansion API
commandLine = getCommandLine()
commandLine.addAll(commandLineParams)
var mainPackMd5: String? = null
var mainPackKey: String? = null
var useApkExpansion = false
val newArgs: MutableList<String> = ArrayList()
var i = 0
while (i < commandLine.size) {
@@ -263,7 +245,7 @@ class Godot(private val context: Context) {
i++
} else if (hasExtra && commandLine[i] == "--apk_expansion_key") {
mainPackKey = commandLine[i + 1]
val prefs = activity.getSharedPreferences(
val prefs = context.getSharedPreferences(
"app_data_keys",
Context.MODE_PRIVATE
)
@@ -288,15 +270,17 @@ class Godot(private val context: Context) {
}
i++
}
var expansionPackPath = ""
commandLine = if (newArgs.isEmpty()) { mutableListOf() } else { newArgs }
if (useApkExpansion && mainPackMd5 != null && mainPackKey != null) {
// Build the full path to the app's expansion files
try {
expansionPackPath = Helpers.getSaveFilePath(context)
expansionPackPath += "/main." + activity.packageManager.getPackageInfo(
activity.packageName,
expansionPackPath += "/main." + context.packageManager.getPackageInfo(
context.packageName,
0
).versionCode + "." + activity.packageName + ".obb"
).versionCode + "." + context.packageName + ".obb"
} catch (e: java.lang.Exception) {
Log.e(TAG, "Unable to build full path to the app's expansion files", e)
}
@@ -317,15 +301,35 @@ class Godot(private val context: Context) {
}
}
initializationStarted = true
} catch (e: java.lang.Exception) {
// Clear the primary host and rethrow
this.primaryHost = null
initializationStarted = false
throw e
} finally {
endBenchmarkMeasure("Startup", "Godot::onCreate")
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::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,7 +503,6 @@ class Godot(private val context: Context) {
override fun onEnd(animation: WindowInsetsAnimationCompat) {}
})
if (host == primaryHost) {
renderView?.queueOnRenderThread {
for (plugin in pluginRegistry.allPlugins) {
plugin.onRegisterPluginWithGodotNative()
@@ -572,7 +521,6 @@ class Godot(private val context: Context) {
}
}
}
}
renderViewInitialized = true
} finally {
if (!renderViewInitialized) {
@@ -615,16 +563,16 @@ class Godot(private val context: Context) {
}
if (accelerometerEnabled.get() && mAccelerometer != null) {
mSensorManager.registerListener(godotInputHandler, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
mSensorManager?.registerListener(godotInputHandler, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
}
if (gravityEnabled.get() && mGravity != null) {
mSensorManager.registerListener(godotInputHandler, mGravity, SensorManager.SENSOR_DELAY_GAME)
mSensorManager?.registerListener(godotInputHandler, mGravity, SensorManager.SENSOR_DELAY_GAME)
}
if (magnetometerEnabled.get() && mMagnetometer != null) {
mSensorManager.registerListener(godotInputHandler, mMagnetometer, SensorManager.SENSOR_DELAY_GAME)
mSensorManager?.registerListener(godotInputHandler, mMagnetometer, SensorManager.SENSOR_DELAY_GAME)
}
if (gyroscopeEnabled.get() && mGyroscope != null) {
mSensorManager.registerListener(godotInputHandler, mGyroscope, SensorManager.SENSOR_DELAY_GAME)
mSensorManager?.registerListener(godotInputHandler, mGyroscope, SensorManager.SENSOR_DELAY_GAME)
}
}
@@ -636,7 +584,7 @@ class Godot(private val context: Context) {
}
renderView?.onActivityPaused()
mSensorManager.unregisterListener(godotInputHandler)
mSensorManager?.unregisterListener(godotInputHandler)
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainPause()
}
@@ -652,16 +600,17 @@ class Godot(private val context: Context) {
}
fun onDestroy(primaryHost: GodotHost) {
Log.v(TAG, "OnDestroy: $primaryHost")
if (this.primaryHost != primaryHost) {
return
}
Log.v(TAG, "OnDestroy: $primaryHost")
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainDestroy()
}
renderView?.onActivityDestroyed()
this.primaryHost = null
}
/**
@@ -721,7 +670,7 @@ class Godot(private val context: Context) {
val overrideVolumeButtons = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/override_volume_buttons"))
val scrollDeadzoneDisabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/disable_scroll_deadzone"))
runOnUiThread {
runOnHostThread {
renderView?.inputHandler?.apply {
enableLongPress(longPressEnabled)
enablePanningAndScalingGestures(panScaleEnabled)
@@ -753,7 +702,7 @@ class Godot(private val context: Context) {
gyroscopeEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gyroscope")))
magnetometerEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_magnetometer")))
runOnUiThread {
runOnHostThread {
registerSensorsIfNeeded()
enableImmersiveMode(useImmersive.get(), true)
}
@@ -782,15 +731,15 @@ class Godot(private val context: Context) {
@StringRes titleResId: Int,
okCallback: Runnable?
) {
val res: Resources = getActivity()?.resources ?: return
val res: Resources = context.resources ?: return
alert(res.getString(messageResId), res.getString(titleResId), okCallback)
}
@JvmOverloads
@Keep
fun alert(message: String, title: String, okCallback: Runnable? = null) {
val activity: Activity = getActivity() ?: return
runOnUiThread {
val activity = getActivity() ?: return
runOnHostThread {
val builder = AlertDialog.Builder(activity)
builder.setMessage(message).setTitle(title)
builder.setPositiveButton(
@@ -814,14 +763,10 @@ class Godot(private val context: Context) {
}
/**
* Runs the specified action on the UI thread.
* If the current thread is the UI thread, then the action is executed immediately.
* If the current thread is not the UI thread, the action is posted to the event queue
* of the UI thread.
* Runs the specified action on the host thread.
*/
fun runOnUiThread(action: Runnable) {
val activity: Activity = getActivity() ?: return
activity.runOnUiThread(action)
fun runOnHostThread(action: Runnable) {
primaryHost?.runOnHostThread(action)
}
/**
@@ -838,7 +783,7 @@ class Godot(private val context: Context) {
var renderingDevice = rendererInfo[0]
var rendererSource = "ProjectSettings"
var renderer = rendererInfo[1]
val cmdline = getCommandLine()
val cmdline = commandLine
var index = cmdline.indexOf("--rendering-method")
if (index > -1 && cmdline.size > index + 1) {
rendererSource = "CommandLine"
@@ -880,7 +825,7 @@ class Godot(private val context: Context) {
}
private fun setKeepScreenOn(enabled: Boolean) {
runOnUiThread {
runOnHostThread {
if (enabled) {
getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
@@ -905,19 +850,21 @@ class Godot(private val context: Context) {
return darkMode
}
@Keep
fun hasClipboard(): Boolean {
return mClipboard.hasPrimaryClip()
return mClipboard?.hasPrimaryClip() == true
}
@Keep
fun getClipboard(): String {
val clipData = mClipboard.primaryClip ?: return ""
val clipData = mClipboard?.primaryClip ?: return ""
val text = clipData.getItemAt(0).text ?: return ""
return text.toString()
}
@Keep
fun setClipboard(text: String?) {
val clip = ClipData.newPlainText("myLabel", text)
mClipboard.setPrimaryClip(clip)
mClipboard?.setPrimaryClip(ClipData.newPlainText("myLabel", text))
}
@Keep
@@ -971,8 +918,7 @@ class Godot(private val context: Context) {
@JvmOverloads
fun destroyAndKillProcess(destroyRunnable: Runnable? = null) {
val host = primaryHost
val activity = host?.activity
if (host == null || activity == null) {
if (host == null) {
// Run the destroyRunnable right away as we are about to force quit.
destroyRunnable?.run()
@@ -984,7 +930,7 @@ class Godot(private val context: Context) {
// Store the destroyRunnable so it can be run when the engine is terminating
runOnTerminate.set(destroyRunnable)
runOnUiThread {
runOnHostThread {
onDestroy(host)
}
}
@@ -1019,14 +965,14 @@ class Godot(private val context: Context) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (amplitude <= -1) {
vibratorService.vibrate(
vibratorService?.vibrate(
VibrationEffect.createOneShot(
durationMs.toLong(),
VibrationEffect.DEFAULT_AMPLITUDE
)
)
} else {
vibratorService.vibrate(
vibratorService?.vibrate(
VibrationEffect.createOneShot(
durationMs.toLong(),
amplitude
@@ -1035,7 +981,7 @@ class Godot(private val context: Context) {
}
} else {
// deprecated in API 26
vibratorService.vibrate(durationMs.toLong())
vibratorService?.vibrate(durationMs.toLong())
}
} catch (e: SecurityException) {
Log.w(TAG, "SecurityException: VIBRATE permission not found. Make sure it is declared in the manifest or enabled in the export preset.")
@@ -1043,21 +989,6 @@ class Godot(private val context: Context) {
}
}
private fun getCommandLine(): MutableList<String> {
val commandLine = try {
commandLineFileParser.parseCommandLine(requireActivity().assets.open("_cl_"))
} catch (ignored: Exception) {
mutableListOf()
}
val hostCommandLine = primaryHost?.commandLine
if (!hostCommandLine.isNullOrEmpty()) {
commandLine.addAll(hostCommandLine)
}
return commandLine
}
/**
* Used by the native code (java_godot_wrapper.h) to access the input fallback mapping.
* @return The input fallback mapping for the current XR mode.
@@ -1077,7 +1008,7 @@ class Godot(private val context: Context) {
}
fun getGrantedPermissions(): Array<String?>? {
return PermissionsUtil.getGrantedPermissions(getActivity())
return PermissionsUtil.getGrantedPermissions(context)
}
/**

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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,11 +213,24 @@ public class GodotIO {
public int[] getDisplaySafeArea() {
Rect rect = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
int[] result = new int[4];
View topView = null;
if (godot.getActivity() != null) {
topView = godot.getActivity().getWindow().getDecorView();
} else if (godot.getRenderView() != null) {
topView = godot.getRenderView().getView();
}
if (topView != null) {
topView.getWindowVisibleDisplayFrame(rect);
result[0] = rect.left;
result[1] = rect.top;
result[2] = rect.right;
result[3] = rect.bottom;
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();
WindowInsets insets = topView.getRootWindowInsets();
DisplayCutout cutout = insets.getDisplayCutout();
if (cutout != null) {
int insetLeft = cutout.getSafeInsetLeft();
@@ -208,15 +241,29 @@ public class GodotIO {
result[3] -= insetTop + cutout.getSafeInsetBottom();
}
}
}
return result;
}
public int[] getDisplayCutouts() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return new int[0];
DisplayCutout cutout = activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
if (cutout == null)
}
View topView = null;
if (godot.getActivity() != null) {
topView = godot.getActivity().getWindow().getDecorView();
} else if (godot.getRenderView() != null) {
topView = godot.getRenderView().getView();
}
if (topView == null) {
return new int[0];
}
DisplayCutout cutout = topView.getRootWindowInsets().getDisplayCutout();
if (cutout == null) {
return new int[0];
}
List<Rect> rects = cutout.getBoundingRects();
int cutouts = rects.size();
int[] result = new int[cutouts * 4];
@@ -242,9 +289,6 @@ public class GodotIO {
if (edit != null) {
edit.showKeyboard(p_existing_text, GodotEditText.VirtualKeyboardType.values()[p_type], p_max_input_length, p_cursor_start, p_cursor_end);
}
//InputMethodManager inputMgr = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
//inputMgr.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
public void hideKeyboard() {
@@ -253,6 +297,11 @@ public class GodotIO {
}
public void setScreenOrientation(int p_orientation) {
final Activity activity = godot.getActivity();
if (activity == null) {
return;
}
switch (p_orientation) {
case SCREEN_LANDSCAPE: {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
@@ -279,6 +328,11 @@ public class GodotIO {
}
public int getScreenOrientation() {
final Activity activity = godot.getActivity();
if (activity == null) {
return -1;
}
int orientation = activity.getRequestedOrientation();
switch (orientation) {
case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:
@@ -310,8 +364,17 @@ public class GodotIO {
}
public int getDisplayRotation() {
WindowManager windowManager = (WindowManager)activity.getSystemService(Context.WINDOW_SERVICE);
int rotation = windowManager.getDefaultDisplay().getRotation();
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) {
@@ -319,6 +382,7 @@ public class GodotIO {
} 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();
}
}

View File

@@ -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,

View File

@@ -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()
}

View File

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

View File

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

View File

@@ -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()) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() {
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() {

View File

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