You've already forked godot
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:
@@ -208,7 +208,9 @@ android {
|
|||||||
flavorDimensions 'edition'
|
flavorDimensions 'edition'
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
standard {}
|
standard {
|
||||||
|
getIsDefault().set(true)
|
||||||
|
}
|
||||||
mono {}
|
mono {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ import android.util.Log;
|
|||||||
|
|
||||||
import androidx.core.splashscreen.SplashScreen;
|
import androidx.core.splashscreen.SplashScreen;
|
||||||
|
|
||||||
import com.godot.game.BuildConfig;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Template activity for Godot Android builds.
|
* Template activity for Godot Android builds.
|
||||||
* Feel free to extend and modify this class for your custom logic.
|
* Feel free to extend and modify this class for your custom logic.
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ package org.godotengine.editor
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
|
import org.godotengine.godot.Godot
|
||||||
import org.godotengine.godot.GodotLib
|
import org.godotengine.godot.GodotLib
|
||||||
import org.godotengine.godot.utils.GameMenuUtils
|
import org.godotengine.godot.utils.GameMenuUtils
|
||||||
import org.godotengine.godot.utils.PermissionsUtil
|
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))
|
.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()}")
|
Log.d(TAG, "Relaunching XR project using ${editorWindowInfo.windowClassName} with parameters ${launchingArgs.contentToString()}")
|
||||||
val godot = godot
|
Godot.getInstance(applicationContext).destroyAndKillProcess {
|
||||||
if (godot != null) {
|
|
||||||
godot.destroyAndKillProcess {
|
|
||||||
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
|
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -63,7 +63,6 @@ import org.godotengine.godot.plugin.AndroidRuntimePlugin
|
|||||||
import org.godotengine.godot.plugin.GodotPlugin
|
import org.godotengine.godot.plugin.GodotPlugin
|
||||||
import org.godotengine.godot.plugin.GodotPluginRegistry
|
import org.godotengine.godot.plugin.GodotPluginRegistry
|
||||||
import org.godotengine.godot.tts.GodotTTS
|
import org.godotengine.godot.tts.GodotTTS
|
||||||
import org.godotengine.godot.utils.CommandLineFileParser
|
|
||||||
import org.godotengine.godot.utils.DialogUtils
|
import org.godotengine.godot.utils.DialogUtils
|
||||||
import org.godotengine.godot.utils.GodotNetUtils
|
import org.godotengine.godot.utils.GodotNetUtils
|
||||||
import org.godotengine.godot.utils.PermissionsUtil
|
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
|
* Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its
|
||||||
* lifecycle methods are properly invoked.
|
* 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
|
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
|
// Supported build flavors
|
||||||
const val EDITOR_FLAVOR = "editor"
|
private const val EDITOR_FLAVOR = "editor"
|
||||||
const val TEMPLATE_FLAVOR = "template"
|
private const val TEMPLATE_FLAVOR = "template"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return true if this is an editor build, false if this is a template build
|
* @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 mSensorManager: SensorManager? by lazy { context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager }
|
||||||
private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
private val mClipboard: ClipboardManager? by lazy { context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager }
|
||||||
private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
private val vibratorService: Vibrator? by lazy { context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator }
|
||||||
|
private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() }
|
||||||
private val pluginRegistry: GodotPluginRegistry by lazy {
|
|
||||||
GodotPluginRegistry.getPluginRegistry()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val accelerometerEnabled = AtomicBoolean(false)
|
private val accelerometerEnabled = AtomicBoolean(false)
|
||||||
private val mAccelerometer: Sensor? by lazy {
|
private val mAccelerometer: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) }
|
||||||
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val gravityEnabled = AtomicBoolean(false)
|
private val gravityEnabled = AtomicBoolean(false)
|
||||||
private val mGravity: Sensor? by lazy {
|
private val mGravity: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_GRAVITY) }
|
||||||
mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val magnetometerEnabled = AtomicBoolean(false)
|
private val magnetometerEnabled = AtomicBoolean(false)
|
||||||
private val mMagnetometer: Sensor? by lazy {
|
private val mMagnetometer: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) }
|
||||||
mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val gyroscopeEnabled = AtomicBoolean(false)
|
private val gyroscopeEnabled = AtomicBoolean(false)
|
||||||
private val mGyroscope: Sensor? by lazy {
|
private val mGyroscope: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE) }
|
||||||
mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
|
||||||
}
|
|
||||||
|
|
||||||
val tts = GodotTTS(context)
|
val tts = GodotTTS(context)
|
||||||
val directoryAccessHandler = DirectoryAccessHandler(context)
|
val directoryAccessHandler = DirectoryAccessHandler(context)
|
||||||
val fileAccessHandler = FileAccessHandler(context)
|
val fileAccessHandler = FileAccessHandler(context)
|
||||||
val netUtils = GodotNetUtils(context)
|
val netUtils = GodotNetUtils(context)
|
||||||
private val commandLineFileParser = CommandLineFileParser()
|
|
||||||
private val godotInputHandler = GodotInputHandler(context, this)
|
private val godotInputHandler = GodotInputHandler(context, this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,11 +140,6 @@ class Godot(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
private val runOnTerminate = AtomicReference<Runnable>()
|
private val runOnTerminate = AtomicReference<Runnable>()
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks whether [onCreate] was completed successfully.
|
|
||||||
*/
|
|
||||||
private var initializationStarted = false
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks whether [GodotLib.initialize] was completed successfully.
|
* Tracks whether [GodotLib.initialize] was completed successfully.
|
||||||
*/
|
*/
|
||||||
@@ -176,17 +167,15 @@ class Godot(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
private val godotMainLoopStarted = AtomicBoolean(false)
|
private val godotMainLoopStarted = AtomicBoolean(false)
|
||||||
|
|
||||||
var io: GodotIO? = null
|
val io = GodotIO(this)
|
||||||
|
|
||||||
private var commandLine : MutableList<String> = ArrayList<String>()
|
private var commandLine : MutableList<String> = ArrayList<String>()
|
||||||
private var xrMode = XRMode.REGULAR
|
private var xrMode = XRMode.REGULAR
|
||||||
private var expansionPackPath: String = ""
|
|
||||||
private var useApkExpansion = false
|
|
||||||
private val useImmersive = AtomicBoolean(false)
|
private val useImmersive = AtomicBoolean(false)
|
||||||
private var useDebugOpengl = false
|
private var useDebugOpengl = false
|
||||||
private var darkMode = false
|
private var darkMode = false
|
||||||
|
|
||||||
private var containerLayout: FrameLayout? = null
|
internal var containerLayout: FrameLayout? = null
|
||||||
var renderView: GodotRenderView? = 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.
|
* 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]
|
* Provides access to the primary host [Activity]
|
||||||
*/
|
*/
|
||||||
fun getActivity() = primaryHost?.activity
|
fun getActivity() = primaryHost?.activity
|
||||||
private fun requireActivity() = getActivity() ?: throw IllegalStateException("Host activity must be non-null")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start initialization of the Godot engine.
|
* Start initialization of the Godot engine.
|
||||||
*
|
*
|
||||||
* This must be followed by [onInitNativeLayer] and [onInitRenderView] in that order to complete
|
* This must be followed by [onInitRenderView] to complete initialization of the engine.
|
||||||
* 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)
|
* @throws IllegalArgumentException exception if the specified expansion pack (if any)
|
||||||
* is invalid.
|
* is invalid.
|
||||||
*/
|
*/
|
||||||
fun onCreate(primaryHost: GodotHost) {
|
fun initEngine(commandLineParams: List<String>, hostPlugins: Set<GodotPlugin>): Boolean {
|
||||||
if (this.primaryHost != null || initializationStarted) {
|
if (isNativeInitialized()) {
|
||||||
Log.d(TAG, "OnCreate already invoked")
|
Log.d(TAG, "Engine already initialized")
|
||||||
return
|
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
|
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 {
|
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")
|
Log.v(TAG, "Initializing Godot plugin registry")
|
||||||
val runtimePlugins = mutableSetOf<GodotPlugin>(AndroidRuntimePlugin(this))
|
val runtimePlugins = mutableSetOf<GodotPlugin>(AndroidRuntimePlugin(this))
|
||||||
runtimePlugins.addAll(primaryHost.getHostPlugins(this))
|
runtimePlugins.addAll(hostPlugins)
|
||||||
GodotPluginRegistry.initializePluginRegistry(this, runtimePlugins)
|
GodotPluginRegistry.initializePluginRegistry(this, runtimePlugins)
|
||||||
if (io == null) {
|
|
||||||
io = GodotIO(activity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for apk expansion API
|
// check for apk expansion API
|
||||||
commandLine = getCommandLine()
|
commandLine.addAll(commandLineParams)
|
||||||
var mainPackMd5: String? = null
|
var mainPackMd5: String? = null
|
||||||
var mainPackKey: String? = null
|
var mainPackKey: String? = null
|
||||||
|
var useApkExpansion = false
|
||||||
val newArgs: MutableList<String> = ArrayList()
|
val newArgs: MutableList<String> = ArrayList()
|
||||||
var i = 0
|
var i = 0
|
||||||
while (i < commandLine.size) {
|
while (i < commandLine.size) {
|
||||||
@@ -263,7 +245,7 @@ class Godot(private val context: Context) {
|
|||||||
i++
|
i++
|
||||||
} else if (hasExtra && commandLine[i] == "--apk_expansion_key") {
|
} else if (hasExtra && commandLine[i] == "--apk_expansion_key") {
|
||||||
mainPackKey = commandLine[i + 1]
|
mainPackKey = commandLine[i + 1]
|
||||||
val prefs = activity.getSharedPreferences(
|
val prefs = context.getSharedPreferences(
|
||||||
"app_data_keys",
|
"app_data_keys",
|
||||||
Context.MODE_PRIVATE
|
Context.MODE_PRIVATE
|
||||||
)
|
)
|
||||||
@@ -288,15 +270,17 @@ class Godot(private val context: Context) {
|
|||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expansionPackPath = ""
|
||||||
commandLine = if (newArgs.isEmpty()) { mutableListOf() } else { newArgs }
|
commandLine = if (newArgs.isEmpty()) { mutableListOf() } else { newArgs }
|
||||||
if (useApkExpansion && mainPackMd5 != null && mainPackKey != null) {
|
if (useApkExpansion && mainPackMd5 != null && mainPackKey != null) {
|
||||||
// Build the full path to the app's expansion files
|
// Build the full path to the app's expansion files
|
||||||
try {
|
try {
|
||||||
expansionPackPath = Helpers.getSaveFilePath(context)
|
expansionPackPath = Helpers.getSaveFilePath(context)
|
||||||
expansionPackPath += "/main." + activity.packageManager.getPackageInfo(
|
expansionPackPath += "/main." + context.packageManager.getPackageInfo(
|
||||||
activity.packageName,
|
context.packageName,
|
||||||
0
|
0
|
||||||
).versionCode + "." + activity.packageName + ".obb"
|
).versionCode + "." + context.packageName + ".obb"
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
Log.e(TAG, "Unable to build full path to the app's expansion files", e)
|
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
|
if (expansionPackPath.isNotEmpty()) {
|
||||||
} catch (e: java.lang.Exception) {
|
commandLine.add("--main-pack")
|
||||||
// Clear the primary host and rethrow
|
commandLine.add(expansionPackPath)
|
||||||
this.primaryHost = null
|
}
|
||||||
initializationStarted = false
|
if (!nativeLayerInitializeCompleted) {
|
||||||
throw e
|
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 {
|
} finally {
|
||||||
endBenchmarkMeasure("Startup", "Godot::onCreate")
|
endBenchmarkMeasure("Startup", "Godot::initEngine")
|
||||||
}
|
}
|
||||||
|
return isNativeInitialized()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -368,7 +372,7 @@ class Godot(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
@Keep
|
@Keep
|
||||||
private fun nativeEnableImmersiveMode(enabled: Boolean) {
|
private fun nativeEnableImmersiveMode(enabled: Boolean) {
|
||||||
runOnUiThread {
|
runOnHostThread {
|
||||||
enableImmersiveMode(enabled)
|
enableImmersiveMode(enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,103 +380,51 @@ class Godot(private val context: Context) {
|
|||||||
@Keep
|
@Keep
|
||||||
fun isInImmersiveMode() = useImmersive.get()
|
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.
|
* 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
|
* This must be preceded by [initEngine] to properly initialize the engine.
|
||||||
* initialize the engine.
|
|
||||||
*
|
*
|
||||||
* @param host The [GodotHost] that's initializing the render views
|
* @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
|
* @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.
|
* @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
|
@JvmOverloads
|
||||||
fun onInitRenderView(host: GodotHost, providedContainerLayout: FrameLayout = FrameLayout(host.activity)): FrameLayout? {
|
fun onInitRenderView(host: GodotHost, providedContainerLayout: FrameLayout = FrameLayout(context)): FrameLayout? {
|
||||||
if (!isNativeInitialized()) {
|
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")
|
beginBenchmarkMeasure("Startup", "Godot::onInitRenderView")
|
||||||
|
Log.v(TAG, "OnInitRenderView: $host")
|
||||||
try {
|
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 = providedContainerLayout
|
||||||
containerLayout?.removeAllViews()
|
containerLayout?.removeAllViews()
|
||||||
containerLayout?.layoutParams = ViewGroup.LayoutParams(
|
val layoutParams = containerLayout?.layoutParams ?: ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
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
|
// GodotEditText layout
|
||||||
val editText = GodotEditText(activity)
|
val editText = GodotEditText(context)
|
||||||
editText.layoutParams =
|
editText.layoutParams =
|
||||||
ViewGroup.LayoutParams(
|
ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
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.
|
// Prevent GodotEditText from showing on splash screen on devices with Android 14 or newer.
|
||||||
editText.setBackgroundColor(Color.TRANSPARENT)
|
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"))
|
!isProjectManagerHint() && !isEditorHint() && java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/per_pixel_transparency/allowed"))
|
||||||
Log.d(TAG, "Render view should be transparent: $shouldBeTransparent")
|
Log.d(TAG, "Render view should be transparent: $shouldBeTransparent")
|
||||||
renderView = if (usesVulkan()) {
|
renderView = if (usesVulkan()) {
|
||||||
if (meetsVulkanRequirements(activity.packageManager)) {
|
if (meetsVulkanRequirements(context.packageManager)) {
|
||||||
GodotVulkanRenderView(host, this, godotInputHandler, shouldBeTransparent)
|
GodotVulkanRenderView(this, godotInputHandler, shouldBeTransparent)
|
||||||
} else if (canFallbackToOpenGL()) {
|
} else if (canFallbackToOpenGL()) {
|
||||||
// Fallback to OpenGl.
|
// Fallback to OpenGl.
|
||||||
GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
|
GodotGLRenderView(this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
|
||||||
} else {
|
} else {
|
||||||
throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message))
|
throw IllegalStateException(context.getString(R.string.error_missing_vulkan_requirements_message))
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to OpenGl.
|
// Fallback to OpenGl.
|
||||||
GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
|
GodotGLRenderView(this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
|
||||||
}
|
|
||||||
|
|
||||||
if (host == primaryHost) {
|
|
||||||
renderView?.startRenderer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderView?.let {
|
renderView?.let {
|
||||||
|
it.startRenderer()
|
||||||
containerLayout?.addView(
|
containerLayout?.addView(
|
||||||
it.view,
|
it.view,
|
||||||
ViewGroup.LayoutParams(
|
ViewGroup.LayoutParams(
|
||||||
@@ -513,20 +462,21 @@ class Godot(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editText.setView(renderView)
|
editText.setView(renderView)
|
||||||
io?.setEdit(editText)
|
io.setEdit(editText)
|
||||||
|
|
||||||
|
val activity = host.activity
|
||||||
// Listeners for keyboard height.
|
// 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.
|
// 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 startBottom = 0
|
||||||
var endBottom = 0
|
var endBottom = 0
|
||||||
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
|
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 {
|
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
|
return bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,23 +503,21 @@ class Godot(private val context: Context) {
|
|||||||
override fun onEnd(animation: WindowInsetsAnimationCompat) {}
|
override fun onEnd(animation: WindowInsetsAnimationCompat) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (host == primaryHost) {
|
renderView?.queueOnRenderThread {
|
||||||
renderView?.queueOnRenderThread {
|
|
||||||
for (plugin in pluginRegistry.allPlugins) {
|
|
||||||
plugin.onRegisterPluginWithGodotNative()
|
|
||||||
}
|
|
||||||
setKeepScreenOn(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include the returned non-null views in the Godot view hierarchy.
|
|
||||||
for (plugin in pluginRegistry.allPlugins) {
|
for (plugin in pluginRegistry.allPlugins) {
|
||||||
val pluginView = plugin.onMainCreate(activity)
|
plugin.onRegisterPluginWithGodotNative()
|
||||||
if (pluginView != null) {
|
}
|
||||||
if (plugin.shouldBeOnTop()) {
|
setKeepScreenOn(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")))
|
||||||
containerLayout?.addView(pluginView)
|
}
|
||||||
} else {
|
|
||||||
containerLayout?.addView(pluginView, 0)
|
// Include the returned non-null views in the Godot view hierarchy.
|
||||||
}
|
for (plugin in pluginRegistry.allPlugins) {
|
||||||
|
val pluginView = plugin.onMainCreate(activity)
|
||||||
|
if (pluginView != null) {
|
||||||
|
if (plugin.shouldBeOnTop()) {
|
||||||
|
containerLayout?.addView(pluginView)
|
||||||
|
} else {
|
||||||
|
containerLayout?.addView(pluginView, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -615,16 +563,16 @@ class Godot(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (accelerometerEnabled.get() && mAccelerometer != null) {
|
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) {
|
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) {
|
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) {
|
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()
|
renderView?.onActivityPaused()
|
||||||
mSensorManager.unregisterListener(godotInputHandler)
|
mSensorManager?.unregisterListener(godotInputHandler)
|
||||||
for (plugin in pluginRegistry.allPlugins) {
|
for (plugin in pluginRegistry.allPlugins) {
|
||||||
plugin.onMainPause()
|
plugin.onMainPause()
|
||||||
}
|
}
|
||||||
@@ -652,16 +600,17 @@ class Godot(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onDestroy(primaryHost: GodotHost) {
|
fun onDestroy(primaryHost: GodotHost) {
|
||||||
Log.v(TAG, "OnDestroy: $primaryHost")
|
|
||||||
if (this.primaryHost != primaryHost) {
|
if (this.primaryHost != primaryHost) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Log.v(TAG, "OnDestroy: $primaryHost")
|
||||||
|
|
||||||
for (plugin in pluginRegistry.allPlugins) {
|
for (plugin in pluginRegistry.allPlugins) {
|
||||||
plugin.onMainDestroy()
|
plugin.onMainDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
renderView?.onActivityDestroyed()
|
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 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"))
|
val scrollDeadzoneDisabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/disable_scroll_deadzone"))
|
||||||
|
|
||||||
runOnUiThread {
|
runOnHostThread {
|
||||||
renderView?.inputHandler?.apply {
|
renderView?.inputHandler?.apply {
|
||||||
enableLongPress(longPressEnabled)
|
enableLongPress(longPressEnabled)
|
||||||
enablePanningAndScalingGestures(panScaleEnabled)
|
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")))
|
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")))
|
magnetometerEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_magnetometer")))
|
||||||
|
|
||||||
runOnUiThread {
|
runOnHostThread {
|
||||||
registerSensorsIfNeeded()
|
registerSensorsIfNeeded()
|
||||||
enableImmersiveMode(useImmersive.get(), true)
|
enableImmersiveMode(useImmersive.get(), true)
|
||||||
}
|
}
|
||||||
@@ -782,15 +731,15 @@ class Godot(private val context: Context) {
|
|||||||
@StringRes titleResId: Int,
|
@StringRes titleResId: Int,
|
||||||
okCallback: Runnable?
|
okCallback: Runnable?
|
||||||
) {
|
) {
|
||||||
val res: Resources = getActivity()?.resources ?: return
|
val res: Resources = context.resources ?: return
|
||||||
alert(res.getString(messageResId), res.getString(titleResId), okCallback)
|
alert(res.getString(messageResId), res.getString(titleResId), okCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
@Keep
|
@Keep
|
||||||
fun alert(message: String, title: String, okCallback: Runnable? = null) {
|
fun alert(message: String, title: String, okCallback: Runnable? = null) {
|
||||||
val activity: Activity = getActivity() ?: return
|
val activity = getActivity() ?: return
|
||||||
runOnUiThread {
|
runOnHostThread {
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setMessage(message).setTitle(title)
|
builder.setMessage(message).setTitle(title)
|
||||||
builder.setPositiveButton(
|
builder.setPositiveButton(
|
||||||
@@ -814,14 +763,10 @@ class Godot(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the specified action on the UI thread.
|
* Runs the specified action on the host 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.
|
|
||||||
*/
|
*/
|
||||||
fun runOnUiThread(action: Runnable) {
|
fun runOnHostThread(action: Runnable) {
|
||||||
val activity: Activity = getActivity() ?: return
|
primaryHost?.runOnHostThread(action)
|
||||||
activity.runOnUiThread(action)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -838,7 +783,7 @@ class Godot(private val context: Context) {
|
|||||||
var renderingDevice = rendererInfo[0]
|
var renderingDevice = rendererInfo[0]
|
||||||
var rendererSource = "ProjectSettings"
|
var rendererSource = "ProjectSettings"
|
||||||
var renderer = rendererInfo[1]
|
var renderer = rendererInfo[1]
|
||||||
val cmdline = getCommandLine()
|
val cmdline = commandLine
|
||||||
var index = cmdline.indexOf("--rendering-method")
|
var index = cmdline.indexOf("--rendering-method")
|
||||||
if (index > -1 && cmdline.size > index + 1) {
|
if (index > -1 && cmdline.size > index + 1) {
|
||||||
rendererSource = "CommandLine"
|
rendererSource = "CommandLine"
|
||||||
@@ -880,7 +825,7 @@ class Godot(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setKeepScreenOn(enabled: Boolean) {
|
private fun setKeepScreenOn(enabled: Boolean) {
|
||||||
runOnUiThread {
|
runOnHostThread {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
} else {
|
} else {
|
||||||
@@ -905,19 +850,21 @@ class Godot(private val context: Context) {
|
|||||||
return darkMode
|
return darkMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
fun hasClipboard(): Boolean {
|
fun hasClipboard(): Boolean {
|
||||||
return mClipboard.hasPrimaryClip()
|
return mClipboard?.hasPrimaryClip() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
fun getClipboard(): String {
|
fun getClipboard(): String {
|
||||||
val clipData = mClipboard.primaryClip ?: return ""
|
val clipData = mClipboard?.primaryClip ?: return ""
|
||||||
val text = clipData.getItemAt(0).text ?: return ""
|
val text = clipData.getItemAt(0).text ?: return ""
|
||||||
return text.toString()
|
return text.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
fun setClipboard(text: String?) {
|
fun setClipboard(text: String?) {
|
||||||
val clip = ClipData.newPlainText("myLabel", text)
|
mClipboard?.setPrimaryClip(ClipData.newPlainText("myLabel", text))
|
||||||
mClipboard.setPrimaryClip(clip)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@@ -971,8 +918,7 @@ class Godot(private val context: Context) {
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun destroyAndKillProcess(destroyRunnable: Runnable? = null) {
|
fun destroyAndKillProcess(destroyRunnable: Runnable? = null) {
|
||||||
val host = primaryHost
|
val host = primaryHost
|
||||||
val activity = host?.activity
|
if (host == null) {
|
||||||
if (host == null || activity == null) {
|
|
||||||
// Run the destroyRunnable right away as we are about to force quit.
|
// Run the destroyRunnable right away as we are about to force quit.
|
||||||
destroyRunnable?.run()
|
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
|
// Store the destroyRunnable so it can be run when the engine is terminating
|
||||||
runOnTerminate.set(destroyRunnable)
|
runOnTerminate.set(destroyRunnable)
|
||||||
|
|
||||||
runOnUiThread {
|
runOnHostThread {
|
||||||
onDestroy(host)
|
onDestroy(host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1019,14 +965,14 @@ class Godot(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
if (amplitude <= -1) {
|
if (amplitude <= -1) {
|
||||||
vibratorService.vibrate(
|
vibratorService?.vibrate(
|
||||||
VibrationEffect.createOneShot(
|
VibrationEffect.createOneShot(
|
||||||
durationMs.toLong(),
|
durationMs.toLong(),
|
||||||
VibrationEffect.DEFAULT_AMPLITUDE
|
VibrationEffect.DEFAULT_AMPLITUDE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
vibratorService.vibrate(
|
vibratorService?.vibrate(
|
||||||
VibrationEffect.createOneShot(
|
VibrationEffect.createOneShot(
|
||||||
durationMs.toLong(),
|
durationMs.toLong(),
|
||||||
amplitude
|
amplitude
|
||||||
@@ -1035,7 +981,7 @@ class Godot(private val context: Context) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// deprecated in API 26
|
// deprecated in API 26
|
||||||
vibratorService.vibrate(durationMs.toLong())
|
vibratorService?.vibrate(durationMs.toLong())
|
||||||
}
|
}
|
||||||
} catch (e: SecurityException) {
|
} 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.")
|
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.
|
* 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.
|
* @return The input fallback mapping for the current XR mode.
|
||||||
@@ -1077,7 +1008,7 @@ class Godot(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getGrantedPermissions(): Array<String?>? {
|
fun getGrantedPermissions(): Array<String?>? {
|
||||||
return PermissionsUtil.getGrantedPermissions(getActivity())
|
return PermissionsUtil.getGrantedPermissions(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import android.util.Log
|
|||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import org.godotengine.godot.utils.CommandLineFileParser
|
||||||
import org.godotengine.godot.utils.PermissionsUtil
|
import org.godotengine.godot.utils.PermissionsUtil
|
||||||
import org.godotengine.godot.utils.ProcessPhoenix
|
import org.godotengine.godot.utils.ProcessPhoenix
|
||||||
|
|
||||||
@@ -73,6 +74,13 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
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)
|
val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||||
Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}")
|
Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}")
|
||||||
commandLineParams.addAll(params ?: emptyArray())
|
commandLineParams.addAll(params ?: emptyArray())
|
||||||
@@ -107,12 +115,7 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||||||
|
|
||||||
protected fun triggerRebirth(bundle: Bundle?, intent: Intent) {
|
protected fun triggerRebirth(bundle: Bundle?, intent: Intent) {
|
||||||
// Launch a new activity
|
// Launch a new activity
|
||||||
val godot = godot
|
Godot.getInstance(applicationContext).destroyAndKillProcess {
|
||||||
if (godot != null) {
|
|
||||||
godot.destroyAndKillProcess {
|
|
||||||
ProcessPhoenix.triggerRebirth(this, bundle, intent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ProcessPhoenix.triggerRebirth(this, bundle, intent)
|
ProcessPhoenix.triggerRebirth(this, bundle, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,8 +162,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||||||
intent = newIntent
|
intent = newIntent
|
||||||
|
|
||||||
handleStartIntent(newIntent, false)
|
handleStartIntent(newIntent, false)
|
||||||
|
|
||||||
godotFragment?.onNewIntent(newIntent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
|
private fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
|
||||||
@@ -215,5 +216,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||||||
return GodotFragment()
|
return GodotFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
override fun getCommandLine(): MutableList<String> = commandLineParams
|
override fun getCommandLine(): MutableList<String> = commandLineParams
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,26 +89,14 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
|||||||
private View mCellMessage;
|
private View mCellMessage;
|
||||||
|
|
||||||
private Button mPauseButton;
|
private Button mPauseButton;
|
||||||
private Button mWiFiSettingsButton;
|
|
||||||
|
|
||||||
private FrameLayout godotContainerLayout;
|
private FrameLayout godotContainerLayout;
|
||||||
private boolean mStatePaused;
|
|
||||||
private int mState;
|
private int mState;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private GodotHost parentHost;
|
private GodotHost parentHost;
|
||||||
private Godot godot;
|
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) {
|
private void setState(int newState) {
|
||||||
if (mState != newState) {
|
if (mState != newState) {
|
||||||
mState = newState;
|
mState = newState;
|
||||||
@@ -117,16 +105,10 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setButtonPausedState(boolean paused) {
|
private void setButtonPausedState(boolean paused) {
|
||||||
mStatePaused = paused;
|
|
||||||
int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause;
|
int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause;
|
||||||
mPauseButton.setText(stringResourceID);
|
mPauseButton.setText(stringResourceID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ResultCallback {
|
|
||||||
void callback(int requestCode, int resultCode, Intent data);
|
|
||||||
}
|
|
||||||
public ResultCallback resultCallback;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Godot getGodot() {
|
public Godot getGodot() {
|
||||||
return godot;
|
return godot;
|
||||||
@@ -159,11 +141,6 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
|||||||
@Override
|
@Override
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
if (resultCallback != null) {
|
|
||||||
resultCallback.callback(requestCode, resultCode, data);
|
|
||||||
resultCallback = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
godot.onActivityResult(requestCode, resultCode, data);
|
godot.onActivityResult(requestCode, resultCode, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,14 +162,11 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
|||||||
BenchmarkUtils.beginBenchmarkMeasure("Startup", "GodotFragment::onCreate");
|
BenchmarkUtils.beginBenchmarkMeasure("Startup", "GodotFragment::onCreate");
|
||||||
super.onCreate(icicle);
|
super.onCreate(icicle);
|
||||||
|
|
||||||
final Activity activity = getActivity();
|
|
||||||
mCurrentIntent = activity.getIntent();
|
|
||||||
|
|
||||||
if (parentHost != null) {
|
if (parentHost != null) {
|
||||||
godot = parentHost.getGodot();
|
godot = parentHost.getGodot();
|
||||||
}
|
}
|
||||||
if (godot == null) {
|
if (godot == null) {
|
||||||
godot = new Godot(requireContext());
|
godot = Godot.getInstance(requireContext());
|
||||||
}
|
}
|
||||||
performEngineInitialization();
|
performEngineInitialization();
|
||||||
BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate");
|
BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate");
|
||||||
@@ -200,10 +174,8 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
|||||||
|
|
||||||
private void performEngineInitialization() {
|
private void performEngineInitialization() {
|
||||||
try {
|
try {
|
||||||
godot.onCreate(this);
|
if (!godot.initEngine(getCommandLine(), getHostPlugins(godot))) {
|
||||||
|
throw new IllegalStateException("Unable to initialize Godot engine");
|
||||||
if (!godot.onInitNativeLayer(this)) {
|
|
||||||
throw new IllegalStateException("Unable to initialize engine native layer");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
godotContainerLayout = godot.onInitRenderView(this);
|
godotContainerLayout = godot.onInitRenderView(this);
|
||||||
@@ -257,7 +229,6 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
|||||||
mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard);
|
mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard);
|
||||||
mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular);
|
mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular);
|
||||||
mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton);
|
mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton);
|
||||||
mWiFiSettingsButton = (Button)downloadingExpansionView.findViewById(R.id.wifiSettingsButton);
|
|
||||||
|
|
||||||
return downloadingExpansionView;
|
return downloadingExpansionView;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,16 +76,14 @@ import java.io.InputStream;
|
|||||||
* bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
|
* bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
|
||||||
*/
|
*/
|
||||||
class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
|
class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
|
||||||
private final GodotHost host;
|
|
||||||
private final Godot godot;
|
private final Godot godot;
|
||||||
private final GodotInputHandler inputHandler;
|
private final GodotInputHandler inputHandler;
|
||||||
private final GodotRenderer godotRenderer;
|
private final GodotRenderer godotRenderer;
|
||||||
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
|
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
|
||||||
|
|
||||||
public GodotGLRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl, boolean shouldBeTranslucent) {
|
public GodotGLRenderView(Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl, boolean shouldBeTranslucent) {
|
||||||
super(host.getActivity());
|
super(godot.getContext());
|
||||||
|
|
||||||
this.host = host;
|
|
||||||
this.godot = godot;
|
this.godot = godot;
|
||||||
this.inputHandler = inputHandler;
|
this.inputHandler = inputHandler;
|
||||||
this.godotRenderer = new GodotRenderer();
|
this.godotRenderer = new GodotRenderer();
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import org.godotengine.godot.plugin.GodotPlugin;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
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();
|
Activity getActivity();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,4 +152,18 @@ public interface GodotHost {
|
|||||||
* Invoked on the render thread when an editor workspace has been selected.
|
* Invoked on the render thread when an editor workspace has been selected.
|
||||||
*/
|
*/
|
||||||
default void onEditorWorkspaceSelected(String workspace) {}
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import android.util.Log;
|
|||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.DisplayCutout;
|
import android.view.DisplayCutout;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
import android.view.View;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
@@ -62,7 +63,8 @@ import java.util.Locale;
|
|||||||
public class GodotIO {
|
public class GodotIO {
|
||||||
private static final String TAG = GodotIO.class.getSimpleName();
|
private static final String TAG = GodotIO.class.getSimpleName();
|
||||||
|
|
||||||
private final Activity activity;
|
private final Godot godot;
|
||||||
|
|
||||||
private final String uniqueId;
|
private final String uniqueId;
|
||||||
GodotEditText edit;
|
GodotEditText edit;
|
||||||
|
|
||||||
@@ -74,9 +76,9 @@ public class GodotIO {
|
|||||||
final int SCREEN_SENSOR_PORTRAIT = 5;
|
final int SCREEN_SENSOR_PORTRAIT = 5;
|
||||||
final int SCREEN_SENSOR = 6;
|
final int SCREEN_SENSOR = 6;
|
||||||
|
|
||||||
GodotIO(Activity p_activity) {
|
GodotIO(Godot godot) {
|
||||||
activity = p_activity;
|
this.godot = godot;
|
||||||
String androidId = Settings.Secure.getString(activity.getContentResolver(),
|
String androidId = Settings.Secure.getString(godot.getContext().getContentResolver(),
|
||||||
Settings.Secure.ANDROID_ID);
|
Settings.Secure.ANDROID_ID);
|
||||||
if (androidId == null) {
|
if (androidId == null) {
|
||||||
androidId = "";
|
androidId = "";
|
||||||
@@ -85,12 +87,22 @@ public class GodotIO {
|
|||||||
uniqueId = androidId;
|
uniqueId = androidId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Context getContext() {
|
||||||
|
Context context = godot.getActivity();
|
||||||
|
if (context == null) {
|
||||||
|
context = godot.getContext();
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////
|
/////////////////////////
|
||||||
// MISCELLANEOUS OS IO
|
// MISCELLANEOUS OS IO
|
||||||
/////////////////////////
|
/////////////////////////
|
||||||
|
|
||||||
public int openURI(String uriString) {
|
public int openURI(String uriString) {
|
||||||
try {
|
try {
|
||||||
|
Context context = getContext();
|
||||||
|
|
||||||
Uri dataUri;
|
Uri dataUri;
|
||||||
String dataType = "";
|
String dataType = "";
|
||||||
boolean grantReadUriPermission = false;
|
boolean grantReadUriPermission = false;
|
||||||
@@ -104,14 +116,14 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
File targetFile = new File(filePath);
|
File targetFile = new File(filePath);
|
||||||
dataUri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", targetFile);
|
dataUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", targetFile);
|
||||||
dataType = activity.getContentResolver().getType(dataUri);
|
dataType = context.getContentResolver().getType(dataUri);
|
||||||
} else {
|
} else {
|
||||||
dataUri = Uri.parse(uriString);
|
dataUri = Uri.parse(uriString);
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent intent = new Intent();
|
Intent intent = new Intent();
|
||||||
intent.setAction(Intent.ACTION_VIEW);
|
intent.setAction(Intent.ACTION_VIEW).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
if (TextUtils.isEmpty(dataType)) {
|
if (TextUtils.isEmpty(dataType)) {
|
||||||
intent.setData(dataUri);
|
intent.setData(dataUri);
|
||||||
} else {
|
} else {
|
||||||
@@ -121,7 +133,7 @@ public class GodotIO {
|
|||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.startActivity(intent);
|
context.startActivity(intent);
|
||||||
return Error.OK.toNativeValue();
|
return Error.OK.toNativeValue();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Unable to open uri " + uriString, e);
|
Log.e(TAG, "Unable to open uri " + uriString, e);
|
||||||
@@ -130,7 +142,7 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getCacheDir() {
|
public String getCacheDir() {
|
||||||
return activity.getCacheDir().getAbsolutePath();
|
return getContext().getCacheDir().getAbsolutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTempDir() {
|
public String getTempDir() {
|
||||||
@@ -146,7 +158,7 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getDataDir() {
|
public String getDataDir() {
|
||||||
return activity.getFilesDir().getAbsolutePath();
|
return getContext().getFilesDir().getAbsolutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLocale() {
|
public String getLocale() {
|
||||||
@@ -158,14 +170,14 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getScreenDPI() {
|
public int getScreenDPI() {
|
||||||
return activity.getResources().getDisplayMetrics().densityDpi;
|
return getContext().getResources().getDisplayMetrics().densityDpi;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns bucketized density values.
|
* Returns bucketized density values.
|
||||||
*/
|
*/
|
||||||
public float getScaledDensity() {
|
public float getScaledDensity() {
|
||||||
int densityDpi = activity.getResources().getDisplayMetrics().densityDpi;
|
int densityDpi = getContext().getResources().getDisplayMetrics().densityDpi;
|
||||||
float selectedScaledDensity;
|
float selectedScaledDensity;
|
||||||
if (densityDpi >= DisplayMetrics.DENSITY_XXXHIGH) {
|
if (densityDpi >= DisplayMetrics.DENSITY_XXXHIGH) {
|
||||||
selectedScaledDensity = 4.0f;
|
selectedScaledDensity = 4.0f;
|
||||||
@@ -184,7 +196,15 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public double getScreenRefreshRate(double fallback) {
|
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) {
|
if (display != null) {
|
||||||
return display.getRefreshRate();
|
return display.getRefreshRate();
|
||||||
}
|
}
|
||||||
@@ -193,30 +213,57 @@ public class GodotIO {
|
|||||||
|
|
||||||
public int[] getDisplaySafeArea() {
|
public int[] getDisplaySafeArea() {
|
||||||
Rect rect = new Rect();
|
Rect rect = new Rect();
|
||||||
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
|
int[] result = new int[4];
|
||||||
|
|
||||||
int[] result = { rect.left, rect.top, rect.right, rect.bottom };
|
View topView = null;
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (godot.getActivity() != null) {
|
||||||
WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets();
|
topView = godot.getActivity().getWindow().getDecorView();
|
||||||
DisplayCutout cutout = insets.getDisplayCutout();
|
} else if (godot.getRenderView() != null) {
|
||||||
if (cutout != null) {
|
topView = godot.getRenderView().getView();
|
||||||
int insetLeft = cutout.getSafeInsetLeft();
|
}
|
||||||
int insetTop = cutout.getSafeInsetTop();
|
|
||||||
result[0] = insetLeft;
|
if (topView != null) {
|
||||||
result[1] = insetTop;
|
topView.getWindowVisibleDisplayFrame(rect);
|
||||||
result[2] -= insetLeft + cutout.getSafeInsetRight();
|
result[0] = rect.left;
|
||||||
result[3] -= insetTop + cutout.getSafeInsetBottom();
|
result[1] = rect.top;
|
||||||
|
result[2] = rect.right;
|
||||||
|
result[3] = rect.bottom;
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
WindowInsets insets = topView.getRootWindowInsets();
|
||||||
|
DisplayCutout cutout = insets.getDisplayCutout();
|
||||||
|
if (cutout != null) {
|
||||||
|
int insetLeft = cutout.getSafeInsetLeft();
|
||||||
|
int insetTop = cutout.getSafeInsetTop();
|
||||||
|
result[0] = insetLeft;
|
||||||
|
result[1] = insetTop;
|
||||||
|
result[2] -= insetLeft + cutout.getSafeInsetRight();
|
||||||
|
result[3] -= insetTop + cutout.getSafeInsetBottom();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int[] getDisplayCutouts() {
|
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];
|
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];
|
return new int[0];
|
||||||
|
}
|
||||||
|
DisplayCutout cutout = topView.getRootWindowInsets().getDisplayCutout();
|
||||||
|
if (cutout == null) {
|
||||||
|
return new int[0];
|
||||||
|
}
|
||||||
List<Rect> rects = cutout.getBoundingRects();
|
List<Rect> rects = cutout.getBoundingRects();
|
||||||
int cutouts = rects.size();
|
int cutouts = rects.size();
|
||||||
int[] result = new int[cutouts * 4];
|
int[] result = new int[cutouts * 4];
|
||||||
@@ -242,9 +289,6 @@ public class GodotIO {
|
|||||||
if (edit != null) {
|
if (edit != null) {
|
||||||
edit.showKeyboard(p_existing_text, GodotEditText.VirtualKeyboardType.values()[p_type], p_max_input_length, p_cursor_start, p_cursor_end);
|
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() {
|
public void hideKeyboard() {
|
||||||
@@ -253,6 +297,11 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setScreenOrientation(int p_orientation) {
|
public void setScreenOrientation(int p_orientation) {
|
||||||
|
final Activity activity = godot.getActivity();
|
||||||
|
if (activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (p_orientation) {
|
switch (p_orientation) {
|
||||||
case SCREEN_LANDSCAPE: {
|
case SCREEN_LANDSCAPE: {
|
||||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||||
@@ -279,6 +328,11 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getScreenOrientation() {
|
public int getScreenOrientation() {
|
||||||
|
final Activity activity = godot.getActivity();
|
||||||
|
if (activity == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
int orientation = activity.getRequestedOrientation();
|
int orientation = activity.getRequestedOrientation();
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:
|
case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:
|
||||||
@@ -310,14 +364,24 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getDisplayRotation() {
|
public int getDisplayRotation() {
|
||||||
WindowManager windowManager = (WindowManager)activity.getSystemService(Context.WINDOW_SERVICE);
|
Activity activity = godot.getActivity();
|
||||||
int rotation = windowManager.getDefaultDisplay().getRotation();
|
|
||||||
if (rotation == Surface.ROTATION_90) {
|
Display display = null;
|
||||||
return 90;
|
if (activity != null) {
|
||||||
} else if (rotation == Surface.ROTATION_180) {
|
display = activity.getWindowManager().getDefaultDisplay();
|
||||||
return 180;
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
} else if (rotation == Surface.ROTATION_270) {
|
display = godot.getContext().getDisplay();
|
||||||
return 270;
|
}
|
||||||
|
|
||||||
|
if (display != null) {
|
||||||
|
int rotation = display.getRotation();
|
||||||
|
if (rotation == Surface.ROTATION_90) {
|
||||||
|
return 90;
|
||||||
|
} else if (rotation == Surface.ROTATION_180) {
|
||||||
|
return 180;
|
||||||
|
} else if (rotation == Surface.ROTATION_270) {
|
||||||
|
return 270;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -382,7 +446,7 @@ public class GodotIO {
|
|||||||
return Environment.getExternalStoragePublicDirectory(what).getAbsolutePath();
|
return Environment.getExternalStoragePublicDirectory(what).getAbsolutePath();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return activity.getExternalFilesDir(what).getAbsolutePath();
|
return getContext().getExternalFilesDir(what).getAbsolutePath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class GodotLib {
|
|||||||
/**
|
/**
|
||||||
* Invoked on the main thread to initialize Godot native layer.
|
* 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,
|
Godot p_instance,
|
||||||
AssetManager p_asset_manager,
|
AssetManager p_asset_manager,
|
||||||
GodotIO godotIO,
|
GodotIO godotIO,
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -51,16 +51,14 @@ import androidx.annotation.Keep;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
|
class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
|
||||||
private final GodotHost host;
|
|
||||||
private final Godot godot;
|
private final Godot godot;
|
||||||
private final GodotInputHandler mInputHandler;
|
private final GodotInputHandler mInputHandler;
|
||||||
private final VkRenderer mRenderer;
|
private final VkRenderer mRenderer;
|
||||||
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
|
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
|
||||||
|
|
||||||
public GodotVulkanRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler, boolean shouldBeTranslucent) {
|
public GodotVulkanRenderView(Godot godot, GodotInputHandler inputHandler, boolean shouldBeTranslucent) {
|
||||||
super(host.getActivity());
|
super(godot.getContext());
|
||||||
|
|
||||||
this.host = host;
|
|
||||||
this.godot = godot;
|
this.godot = godot;
|
||||||
mInputHandler = inputHandler;
|
mInputHandler = inputHandler;
|
||||||
mRenderer = new VkRenderer();
|
mRenderer = new VkRenderer();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import org.godotengine.godot.BuildConfig;
|
|||||||
import org.godotengine.godot.Godot;
|
import org.godotengine.godot.Godot;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -46,10 +47,8 @@ import androidx.annotation.Nullable;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@@ -109,6 +108,13 @@ public abstract class GodotPlugin {
|
|||||||
return godot.getActivity();
|
return godot.getActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to the {@link Context}.
|
||||||
|
*/
|
||||||
|
protected Context getContext() {
|
||||||
|
return godot.getContext();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the plugin with Godot native code.
|
* Register the plugin with Godot native code.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -179,7 +185,7 @@ public abstract class GodotPlugin {
|
|||||||
* @return the plugin's view to be included; null if no views should be included.
|
* @return the plugin's view to be included; null if no views should be included.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public View onMainCreate(Activity activity) {
|
public View onMainCreate(@Nullable Activity activity) {
|
||||||
return null;
|
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
|
* Runs the specified action on the host thread.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* @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) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ package org.godotengine.godot.plugin;
|
|||||||
|
|
||||||
import org.godotengine.godot.Godot;
|
import org.godotengine.godot.Godot;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.content.Context;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -134,10 +134,10 @@ public final class GodotPluginRegistry {
|
|||||||
|
|
||||||
// Register the manifest plugins
|
// Register the manifest plugins
|
||||||
try {
|
try {
|
||||||
final Activity activity = godot.getActivity();
|
final Context context = godot.getContext();
|
||||||
ApplicationInfo appInfo = activity
|
ApplicationInfo appInfo = context
|
||||||
.getPackageManager()
|
.getPackageManager()
|
||||||
.getApplicationInfo(activity.getPackageName(),
|
.getApplicationInfo(context.getPackageName(),
|
||||||
PackageManager.GET_META_DATA);
|
PackageManager.GET_META_DATA);
|
||||||
Bundle metaData = appInfo.metaData;
|
Bundle metaData = appInfo.metaData;
|
||||||
if (metaData == null || metaData.isEmpty()) {
|
if (metaData == null || metaData.isEmpty()) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ import java.util.ArrayList
|
|||||||
*
|
*
|
||||||
* Returns a mutable list of command lines
|
* Returns a mutable list of command lines
|
||||||
*/
|
*/
|
||||||
internal class CommandLineFileParser {
|
internal object CommandLineFileParser {
|
||||||
fun parseCommandLine(inputStream: InputStream): MutableList<String> {
|
fun parseCommandLine(inputStream: InputStream): MutableList<String> {
|
||||||
return try {
|
return try {
|
||||||
val headerBytes = ByteArray(4)
|
val headerBytes = ByteArray(4)
|
||||||
|
|||||||
@@ -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;
|
JavaVM *jvm;
|
||||||
env->GetJavaVM(&jvm);
|
env->GetJavaVM(&jvm);
|
||||||
|
|
||||||
// create our wrapper classes
|
// 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);
|
godot_io_java = new GodotIOJavaWrapper(env, p_godot_io);
|
||||||
|
|
||||||
init_thread_jandroid(jvm, env);
|
init_thread_jandroid(jvm, env);
|
||||||
|
|||||||
@@ -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.
|
// 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)
|
// 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" {
|
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 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 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);
|
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jobject p_surface, jint p_width, jint p_height);
|
||||||
|
|||||||
@@ -37,9 +37,8 @@
|
|||||||
|
|
||||||
// TODO we could probably create a base class for this...
|
// 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);
|
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...
|
// get info about our Godot class so we can get pointers and stuff...
|
||||||
godot_class = p_env->FindClass("org/godotengine/godot/Godot");
|
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
|
// this is a pretty serious fail.. bail... pointers will stay 0
|
||||||
return;
|
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...
|
// get some Godot method pointers...
|
||||||
_restart = p_env->GetMethodID(godot_class, "restart", "()V");
|
_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");
|
_enable_immersive_mode = p_env->GetMethodID(godot_class, "nativeEnableImmersiveMode", "(Z)V");
|
||||||
_is_in_immersive_mode = p_env->GetMethodID(godot_class, "isInImmersiveMode", "()Z");
|
_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");
|
_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() {
|
GodotJavaWrapper::~GodotJavaWrapper() {
|
||||||
@@ -105,12 +98,16 @@ GodotJavaWrapper::~GodotJavaWrapper() {
|
|||||||
ERR_FAIL_NULL(env);
|
ERR_FAIL_NULL(env);
|
||||||
env->DeleteGlobalRef(godot_instance);
|
env->DeleteGlobalRef(godot_instance);
|
||||||
env->DeleteGlobalRef(godot_class);
|
env->DeleteGlobalRef(godot_class);
|
||||||
env->DeleteGlobalRef(activity);
|
|
||||||
env->DeleteGlobalRef(activity_class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jobject GodotJavaWrapper::get_activity() {
|
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() {
|
GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() {
|
||||||
|
|||||||
@@ -42,9 +42,7 @@
|
|||||||
class GodotJavaWrapper {
|
class GodotJavaWrapper {
|
||||||
private:
|
private:
|
||||||
jobject godot_instance;
|
jobject godot_instance;
|
||||||
jobject activity;
|
|
||||||
jclass godot_class;
|
jclass godot_class;
|
||||||
jclass activity_class;
|
|
||||||
|
|
||||||
GodotJavaViewWrapper *godot_view = nullptr;
|
GodotJavaViewWrapper *godot_view = nullptr;
|
||||||
|
|
||||||
@@ -84,9 +82,10 @@ private:
|
|||||||
jmethodID _enable_immersive_mode = nullptr;
|
jmethodID _enable_immersive_mode = nullptr;
|
||||||
jmethodID _is_in_immersive_mode = nullptr;
|
jmethodID _is_in_immersive_mode = nullptr;
|
||||||
jmethodID _on_editor_workspace_selected = nullptr;
|
jmethodID _on_editor_workspace_selected = nullptr;
|
||||||
|
jmethodID _get_activity = nullptr;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
|
GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance);
|
||||||
~GodotJavaWrapper();
|
~GodotJavaWrapper();
|
||||||
|
|
||||||
jobject get_activity();
|
jobject get_activity();
|
||||||
|
|||||||
Reference in New Issue
Block a user