Merged upstream 4.7 into downstream 4.6
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled

This commit is contained in:
2026-06-27 13:51:14 -04:00
5981 changed files with 851233 additions and 341989 deletions
+20 -20
View File
@@ -27,8 +27,6 @@ allprojects {
}
configurations {
// Initializes a placeholder for the devImplementation dependency configuration.
devImplementation {}
// Initializes a placeholder for the monoImplementation dependency configuration.
monoImplementation {}
}
@@ -43,6 +41,7 @@ dependencies {
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
implementation "androidx.documentfile:documentfile:$versions.documentfileVersion"
if (rootProject.findProject(":lib")) {
implementation project(":lib")
@@ -52,15 +51,32 @@ dependencies {
// Godot gradle build mode. In this scenario this project is the only one around and the Godot
// library is available through the pre-generated godot-lib.*.aar android archive files.
debugImplementation fileTree(dir: 'libs/debug', include: ['**/*.jar', '*.aar'])
devImplementation fileTree(dir: 'libs/dev', include: ['**/*.jar', '*.aar'])
releaseImplementation fileTree(dir: 'libs/release', include: ['**/*.jar', '*.aar'])
}
// Godot user plugins remote dependencies
String[] remoteDeps = getGodotPluginsRemoteBinaries()
if (remoteDeps != null && remoteDeps.size() > 0) {
def platformPattern = /^\s*(platform|enforcedPlatform)\s*\(\s*['"]*(\S+)['"]*\s*\)$/
for (String dep : remoteDeps) {
implementation dep
def matcher = dep =~ platformPattern
if (matcher) {
switch (matcher[0][1]) {
case "platform":
implementation platform(matcher[0][2])
break
case "enforcedPlatform":
implementation enforcedPlatform(matcher[0][2])
break
default:
throw new GradleException("Invalid remote platform dependency: $dep")
break
}
} else {
implementation dep
}
}
}
@@ -205,18 +221,6 @@ android {
}
}
dev {
initWith debug
// Signing and zip-aligning are skipped for prebuilt builds, but
// performed for Godot gradle builds.
zipAlignEnabled shouldZipAlign()
if (shouldSign()) {
signingConfig signingConfigs.debug
} else {
signingConfig null
}
}
release {
// Signing and zip-aligning are skipped for prebuilt builds, but
// performed for Godot gradle builds.
@@ -250,7 +254,6 @@ android {
sourceSets {
main.res.srcDirs += ['res']
debug.jniLibs.srcDirs = ['libs/debug', 'libs/debug/vulkan_validation_layers']
dev.jniLibs.srcDirs = ['libs/dev']
release.jniLibs.srcDirs = ['libs/release']
}
@@ -329,9 +332,6 @@ module, so we're ensuring the ':app:preBuild' task is set to run after those tas
if (rootProject.tasks.findByPath("copyDebugAARToAppModule") != null) {
preBuild.mustRunAfter(rootProject.tasks.named("copyDebugAARToAppModule"))
}
if (rootProject.tasks.findByPath("copyDevAARToAppModule") != null) {
preBuild.mustRunAfter(rootProject.tasks.named("copyDevAARToAppModule"))
}
if (rootProject.tasks.findByPath("copyReleaseAARToAppModule") != null) {
preBuild.mustRunAfter(rootProject.tasks.named("copyReleaseAARToAppModule"))
}
+7 -6
View File
@@ -1,28 +1,29 @@
ext.versions = [
androidGradlePlugin: '8.6.1',
compileSdk : 35,
compileSdk : 36,
// Also update:
// - 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION'
// - 'platform/android/detect.py#get_min_target_api()'
minSdk : 24,
// Also update 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION'
targetSdk : 35,
buildTools : '35.0.1',
kotlinVersion : '2.1.20',
targetSdk : 36,
buildTools : '36.1.0',
kotlinVersion : '2.1.21',
fragmentVersion : '1.8.6',
nexusPublishVersion: '1.3.0',
javaVersion : JavaVersion.VERSION_17,
// Also update 'platform/android/detect.py#get_ndk_version()' when this is updated.
ndkVersion : '28.1.13356709',
ndkVersion : '29.0.14206865',
splashscreenVersion: '1.0.1',
// 'openxrLoaderVersion' should be set to XR_CURRENT_API_VERSION, see 'thirdparty/openxr'
openxrLoaderVersion: '1.1.53',
openxrVendorsVersion: '4.2.2-stable',
openxrVendorsVersion: '4.3.0-stable',
junitVersion : '1.3.0',
espressoCoreVersion: '3.7.0',
kotlinTestVersion : '1.3.11',
testRunnerVersion : '1.7.0',
testOrchestratorVersion: '1.6.1',
documentfileVersion: '1.1.0',
]
ext.getExportPackageName = { ->
@@ -11,7 +11,8 @@
To add custom attributes, use the "gradle_build/custom_theme_attributes" Android export option. -->
<style name="GodotAppSplashTheme" parent="Theme.SplashScreen">
<item name="android:windowSplashScreenBackground">@mipmap/icon_background</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/icon_foreground</item>
<item name="android:windowSplashScreenBrandingImage">@drawable/splash_branding_image</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="postSplashScreenTheme">@style/GodotAppMainTheme</item>
<item name="android:windowIsTranslucent">false</item>
</style>
@@ -34,8 +34,11 @@ import android.content.ComponentName
import android.content.Intent
import android.util.Log
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.godot.game.test.GodotAppInstrumentedTestPlugin
import org.godotengine.godot.Godot
import org.godotengine.godot.GodotActivity.Companion.EXTRA_COMMAND_LINE_PARAMS
import org.godotengine.godot.plugin.GodotPluginRegistry
import org.junit.Test
@@ -110,6 +113,28 @@ class GodotAppTest {
}
}
/**
* Runs test to validate android plugin signals.
*/
@Test
fun runPluginSignalTests() {
ActivityScenario.launch(GodotApp::class.java).use { scenario ->
scenario.onActivity { activity ->
val testPlugin = getTestPlugin()
assertNotNull(testPlugin)
Log.d(TAG, "Waiting for the Godot main loop to start...")
testPlugin.waitForGodotMainLoopStarted()
Log.d(TAG, "Running Android plugin signal tests...")
val result = testPlugin.runPluginSignalTests()
assertNotNull(result)
result.exceptionOrNull()?.let { throw it }
assertTrue(result.isSuccess)
}
}
}
/**
* Test implicit launch of the Godot app, and validates this resolves to the `GodotAppLauncher` activity alias.
*/
@@ -169,4 +194,56 @@ class GodotAppTest {
}
}
}
/**
* Validate that the back press does not quit the game when 'quit_on_go_back' is disabled.
*/
@Test
fun testGameNotQuittingOnBackPress() {
ActivityScenario.launch(GodotApp::class.java).use { scenario ->
val testPlugin = getTestPlugin()
assertNotNull(testPlugin)
Log.d(TAG, "Waiting for the Godot main loop to start...")
testPlugin.waitForGodotMainLoopStarted()
// Disable 'quit_on_go_back'.
testPlugin.updateQuitOnGoBack(false)
// Trigger the back press event.
Espresso.pressBackUnconditionally()
Log.d(TAG, "Waiting for the engine to terminate...")
testPlugin.waitForEngineTermination(5_000L)
val godot = Godot.getInstance(InstrumentationRegistry.getInstrumentation().targetContext)
assertTrue { godot.runStatus != Godot.RunStatus.TERMINATING }
}
}
/**
* Validate that the back press event quits the game when 'quit_on_go_back' is enabled.
*/
@Test
fun testGameQuittingOnBackPress() {
ActivityScenario.launch(GodotApp::class.java).use { scenario ->
val testPlugin = getTestPlugin()
assertNotNull(testPlugin)
Log.d(TAG, "Waiting for the Godot main loop to start...")
testPlugin.waitForGodotMainLoopStarted()
// Enable 'quit_on_go_back'.
testPlugin.updateQuitOnGoBack(true)
// Trigger the back press event.
Espresso.pressBackUnconditionally()
Log.d(TAG, "Waiting for the engine to terminate...")
testPlugin.waitForEngineTermination(5_000L)
val godot = Godot.getInstance(InstrumentationRegistry.getInstrumentation().targetContext)
assertTrue { godot.runStatus == Godot.RunStatus.TERMINATING }
}
}
}
@@ -4,6 +4,10 @@
<meta-data
android:name="org.godotengine.plugin.v2.GodotAppInstrumentedTestPlugin"
android:value="com.godot.game.test.GodotAppInstrumentedTestPlugin"/>
<meta-data
android:name="org.godotengine.plugin.v2.SignalTestPlugin"
android:value="com.godot.game.test.SignalTestPlugin"/>
</application>
</manifest>
@@ -1,4 +1,12 @@
list=[{
"base": &"BaseTest",
"class": &"AndroidPluginSignalTests",
"icon": "",
"is_abstract": false,
"is_tool": false,
"language": &"GDScript",
"path": "res://test/android_plugin/signal_tests.gd"
}, {
"base": &"RefCounted",
"class": &"BaseTest",
"icon": "",
@@ -3,12 +3,22 @@ extends Node2D
var _plugin_name = "GodotAppInstrumentedTestPlugin"
var _android_plugin
func _ready():
var _signal_test_plugin_name = "SignalTestPlugin"
var _signal_test_plugin
func _init():
# Verify plugin singleton in _init, since plugins should already be registered at this point.
if Engine.has_singleton(_plugin_name):
_android_plugin = Engine.get_singleton(_plugin_name)
_android_plugin.connect("launch_tests", _launch_tests)
else:
printerr("Couldn't find plugin " + _plugin_name)
_android_plugin.connect("update_quit_on_go_back", _update_quit_on_go_back)
if Engine.has_singleton(_signal_test_plugin_name):
_signal_test_plugin = Engine.get_singleton(_signal_test_plugin_name)
func _ready() -> void:
if not _android_plugin:
printerr("ERROR: Couldn't find plugin " + _plugin_name)
get_tree().quit()
func _launch_tests(test_label: String) -> void:
@@ -18,16 +28,22 @@ func _launch_tests(test_label: String) -> void:
test_instance = JavaClassWrapperTests.new()
"file_access_tests":
test_instance = FileAccessTests.new()
"android_plugin_signal_tests":
test_instance = AndroidPluginSignalTests.new(_signal_test_plugin)
if test_instance:
test_instance.__reset_tests()
test_instance.run_tests()
await test_instance.run_tests()
var incomplete_tests = test_instance._test_started - test_instance._test_completed
_android_plugin.onTestsCompleted(test_label, test_instance._test_completed, test_instance._test_assert_failures + incomplete_tests)
else:
_android_plugin.onTestsFailed(test_label, "Unable to launch tests")
func _update_quit_on_go_back(quit_on_go_back: bool) -> void:
get_tree().quit_on_go_back = quit_on_go_back
func _on_plugin_toast_button_pressed() -> void:
if _android_plugin:
_android_plugin.helloWorld()
@@ -1,29 +1,29 @@
[gd_scene load_steps=2 format=3 uid="uid://cg3hylang5fxn"]
[gd_scene format=3 uid="uid://cg3hylang5fxn"]
[ext_resource type="Script" uid="uid://bv6y7in6otgcm" path="res://main.gd" id="1_j0gfq"]
[node name="Main" type="Node2D"]
[node name="Main" type="Node2D" unique_id=852911723]
script = ExtResource("1_j0gfq")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1839352715]
offset_left = 68.0
offset_top = 102.0
offset_right = 506.0
offset_bottom = 408.0
theme_override_constants/separation = 25
[node name="PluginToastButton" type="Button" parent="VBoxContainer"]
[node name="PluginToastButton" type="Button" parent="VBoxContainer" unique_id=1670434164]
custom_minimum_size = Vector2(0, 50)
layout_mode = 2
text = "Plugin Toast
"
[node name="VibrationButton" type="Button" parent="VBoxContainer"]
[node name="VibrationButton" type="Button" parent="VBoxContainer" unique_id=648980813]
custom_minimum_size = Vector2(0, 50)
layout_mode = 2
text = "Vibration"
[node name="GDScriptToastButton" type="Button" parent="VBoxContainer"]
[node name="GDScriptToastButton" type="Button" parent="VBoxContainer" unique_id=95554078]
custom_minimum_size = Vector2(0, 50)
layout_mode = 2
text = "GDScript Toast
@@ -8,11 +8,15 @@
config_version=5
[animation]
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application]
config/name="Godot App Instrumentation Tests"
run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.5", "GL Compatibility")
config/features=PackedStringArray("4.7", "GL Compatibility")
config/icon="res://icon.svg"
[debug]
@@ -0,0 +1,76 @@
class_name AndroidPluginSignalTests
extends BaseTest
var _plugin: JNISingleton
const emission_test_signal = "emission_test_signal"
const launch_test_signal = "launch_tests"
signal emission_test_signal_emitted
func _init(plugin: JNISingleton) -> void:
_plugin = plugin
func run_tests():
print("Android plugin signal tests starting...")
__exec_test(test_plugin_exists)
__exec_test(test_signal_registration)
__exec_test(test_signal_connection)
await __exec_test(test_signal_emission)
print("Android plugin signal tests completed.")
func test_plugin_exists() -> bool:
if _plugin == null:
printerr("ERROR: Couldn't find SignalTestPlugin plugin; _plugin is null")
return false
return true
func test_signal_registration() -> bool:
var signal_registered = _plugin.has_signal(emission_test_signal)
assert_true(signal_registered)
var launch_signal_registered = _plugin.has_signal(launch_test_signal)
assert_true(launch_signal_registered)
return true
func test_signal_connection() -> bool:
_plugin.connect(emission_test_signal, _on_emission_test_signal_emitted)
assert_equal(_plugin.has_connections(emission_test_signal), true)
_plugin.disconnect(emission_test_signal, _on_emission_test_signal_emitted)
assert_equal(_plugin.has_connections(emission_test_signal), false)
_plugin.emission_test_signal.connect(_on_emission_test_signal_emitted)
assert_equal(_plugin.has_connections(emission_test_signal), true)
_plugin.emission_test_signal.disconnect(_on_emission_test_signal_emitted)
assert_equal(_plugin.has_connections(emission_test_signal), false)
return true
func test_signal_emission() -> bool:
var err1 = _plugin.connect(emission_test_signal, _on_emission_test_signal_emitted)
assert_equal(err1, OK)
_plugin.triggerTestSignal1()
await emission_test_signal_emitted
# Test case: Same signal name, but different type and number of parameters
# The "launch_tests" signal is registered by both GodotAppInstrumentedTestPlugin and SignalTestPlugin.
# SignalTestPlugin emits it with a boolean and a string arguments, while GodotAppInstrumentedTestPlugin emits it with one string.
var err2 = _plugin.connect(launch_test_signal, _on_launch_tests_emitted)
assert_equal(err2, OK)
_plugin.triggerLaunchTestSignal()
await emission_test_signal_emitted
return true
func _on_emission_test_signal_emitted() -> void:
emission_test_signal_emitted.emit()
func _on_launch_tests_emitted(param1: bool, param2: String) -> void:
assert_true(param1)
assert_equal(param2, "second message")
emission_test_signal_emitted.emit()
@@ -0,0 +1 @@
uid://cq0mxqlbbug6r
@@ -9,8 +9,9 @@ var _test_assert_failures := 0
func __exec_test(test_func: Callable):
_test_started += 1
test_func.call()
_test_completed += 1
var ret = await test_func.call()
if ret == true:
_test_completed += 1
func __reset_tests():
_test_started = 0
@@ -9,10 +9,14 @@ func run_tests():
__exec_test(test_internal_app_dir_access)
__exec_test(test_internal_cache_dir_access)
__exec_test(test_external_app_dir_access)
__exec_test(test_downloads_dir_access)
__exec_test(test_documents_dir_access)
func _test_dir_access(dir_path: String, data_file_content: String) -> void:
# Scoped storage: Testing access to Downloads and Documents directory.
var version = JavaClassWrapper.wrap("android.os.Build$VERSION")
if version.SDK_INT >= 30:
__exec_test(test_downloads_dir_access)
__exec_test(test_documents_dir_access)
func _test_dir_access(dir_path: String, data_file_content: String) -> bool:
print("Testing access to " + dir_path)
var data_file_path = dir_path.path_join("data.dat")
@@ -29,45 +33,52 @@ func _test_dir_access(dir_path: String, data_file_content: String) -> void:
var deletion_result = DirAccess.remove_absolute(data_file_path)
assert_equal(deletion_result, OK)
return true
func test_obb_dir_access() -> void:
func test_obb_dir_access() -> bool:
var android_runtime = Engine.get_singleton("AndroidRuntime")
assert_true(android_runtime != null)
var app_context = android_runtime.getApplicationContext()
var obb_dir: String = app_context.getObbDir().getCanonicalPath()
_test_dir_access(obb_dir, FILE_CONTENT + "obb dir.")
return true
func test_internal_app_dir_access() -> void:
func test_internal_app_dir_access() -> bool:
var android_runtime = Engine.get_singleton("AndroidRuntime")
assert_true(android_runtime != null)
var app_context = android_runtime.getApplicationContext()
var internal_app_dir: String = app_context.getFilesDir().getCanonicalPath()
_test_dir_access(internal_app_dir, FILE_CONTENT + "internal app dir.")
return true
func test_internal_cache_dir_access() -> void:
func test_internal_cache_dir_access() -> bool:
var android_runtime = Engine.get_singleton("AndroidRuntime")
assert_true(android_runtime != null)
var app_context = android_runtime.getApplicationContext()
var internal_cache_dir: String = app_context.getCacheDir().getCanonicalPath()
_test_dir_access(internal_cache_dir, FILE_CONTENT + "internal cache dir.")
return true
func test_external_app_dir_access() -> void:
func test_external_app_dir_access() -> bool:
var android_runtime = Engine.get_singleton("AndroidRuntime")
assert_true(android_runtime != null)
var app_context = android_runtime.getApplicationContext()
var external_app_dir: String = app_context.getExternalFilesDir("").getCanonicalPath()
_test_dir_access(external_app_dir, FILE_CONTENT + "external app dir.")
return true
func test_downloads_dir_access() -> void:
func test_downloads_dir_access() -> bool:
var EnvironmentClass = JavaClassWrapper.wrap("android.os.Environment")
var downloads_dir = EnvironmentClass.getExternalStoragePublicDirectory(EnvironmentClass.DIRECTORY_DOWNLOADS).getCanonicalPath()
_test_dir_access(downloads_dir, FILE_CONTENT + "downloads dir.")
return true
func test_documents_dir_access() -> void:
func test_documents_dir_access() -> bool:
var EnvironmentClass = JavaClassWrapper.wrap("android.os.Environment")
var documents_dir = EnvironmentClass.getExternalStoragePublicDirectory(EnvironmentClass.DIRECTORY_DOCUMENTS).getCanonicalPath()
_test_dir_access(documents_dir, FILE_CONTENT + "documents dir.")
return true
@@ -20,12 +20,16 @@ func run_tests():
__exec_test(test_callable)
__exec_test(test_interface_callable_proxy)
__exec_test(test_interface_object_proxy)
print("JavaClassWrapper tests finished.")
print("Tests started: " + str(_test_started))
print("Tests completed: " + str(_test_completed))
func test_exceptions() -> void:
func test_exceptions() -> bool:
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
#print(TestClass.get_java_method_list())
@@ -36,7 +40,9 @@ func test_exceptions() -> void:
assert_equal(JavaClassWrapper.get_exception(), null)
func test_multiple_signatures() -> void:
return true
func test_multiple_signatures() -> bool:
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
var ai := [1, 2]
@@ -55,7 +61,9 @@ func test_multiple_signatures() -> void:
]
assert_equal(TestClass.testMethod(3, aobjl), "testObjects: 27 135")
func test_array_arguments() -> void:
return true
func test_array_arguments() -> bool:
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
assert_equal(TestClass.testArgBoolArray([true, false, true]), "[true, false, true]")
@@ -72,7 +80,9 @@ func test_array_arguments() -> void:
assert_equal(TestClass.testArgDoubleArray(PackedFloat64Array([37.1, 38.2, 39.3])), "[37.1, 38.2, 39.3]")
assert_equal(TestClass.testArgDoubleArray([37.1, 38.2, 39.3]), "[37.1, 38.2, 39.3]")
func test_array_return() -> void:
return true
func test_array_return() -> bool:
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
#print(TestClass.get_java_method_list())
@@ -107,14 +117,17 @@ func test_array_return() -> void:
assert_equal(TestClass.testRetStringArray(), PackedStringArray(["I", "am", "String"]))
assert_equal(TestClass.testRetCharSequenceArray(), PackedStringArray(["I", "am", "CharSequence"]))
func test_dictionary():
return true
func test_dictionary() -> bool:
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
assert_equal(TestClass.testDictionary({a = 1, b = 2}), "{a=1, b=2}")
assert_equal(TestClass.testRetDictionary(), {a = 1, b = 2})
assert_equal(TestClass.testRetDictionaryArray(), [{a = 1, b = 2}])
assert_equal(TestClass.testDictionaryNested({a = 1, b = [2, 3], c = 4}), "{a: 1, b: [2, 3], c: 4}")
return true
func test_object_overload():
func test_object_overload() -> bool:
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
var TestClass2: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass2')
var TestClass3: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass3')
@@ -130,22 +143,25 @@ func test_object_overload():
assert_equal(TestClass.testObjectOverloadArray(arr_of_t2), "TestClass2: [33, 34]")
assert_equal(TestClass.testObjectOverloadArray(arr_of_t3), "TestClass3: [thirty three, thirty four]")
return true
func test_variant_conversion_safe_from_stack_overflow():
func test_variant_conversion_safe_from_stack_overflow() -> bool:
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
var arr: Array = [42]
var dict: Dictionary = {"arr": arr}
arr.append(dict)
# The following line will crash with stack overflow if not handled property:
TestClass.testDictionary(dict)
return true
func test_big_integers():
func test_big_integers() -> bool:
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
assert_equal(TestClass.testArgLong(4242424242), "4242424242")
assert_equal(TestClass.testArgLong(-4242424242), "-4242424242")
assert_equal(TestClass.testDictionary({a = 4242424242, b = -4242424242}), "{a=4242424242, b=-4242424242}")
return true
func test_callable():
func test_callable() -> bool:
var android_runtime = Engine.get_singleton("AndroidRuntime")
assert_true(android_runtime != null)
@@ -155,3 +171,36 @@ func test_callable():
return null
android_runtime.createRunnableFromGodotCallable(cb1).run()
assert_equal(cb1_data['called'], true)
return true
func test_interface_callable_proxy() -> bool:
var cb1_data := {called = false, content = ""}
var cb1 = func (content: String) -> void:
cb1_data['called'] = true
cb1_data['content'] = content
var printer_proxy = JavaClassWrapper.create_sam_callback("android.util.Printer", cb1)
assert_true(printer_proxy != null)
printer_proxy.println("This is a callback test")
assert_equal(cb1_data['called'], true)
assert_equal(cb1_data['content'], "This is a callback test")
return true
class PrintProxy:
var test_data := {called = false, content = ""}
func println(content: String) -> void:
test_data['called'] = true
test_data['content'] = content
func test_interface_object_proxy() -> bool:
var print_object = PrintProxy.new()
var proxy = JavaClassWrapper.create_proxy(print_object, ["android.util.Printer"])
assert_true(proxy != null)
proxy.println("This is proxy test")
assert_equal(print_object.test_data['called'], true)
assert_equal(print_object.test_data['content'], "This is proxy test")
return true
@@ -38,6 +38,7 @@ import org.godotengine.godot.plugin.UsedByGodot
import org.godotengine.godot.plugin.SignalInfo
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* [GodotPlugin] used to drive instrumented tests.
@@ -47,14 +48,18 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
companion object {
private val TAG = GodotAppInstrumentedTestPlugin::class.java.simpleName
private const val MAIN_LOOP_STARTED_LATCH_KEY = "main_loop_started_latch"
private const val ENGINE_TERMINATING_LATCH_KEY = "engine_terminating_latch"
private const val JAVACLASSWRAPPER_TESTS = "javaclasswrapper_tests"
private const val FILE_ACCESS_TESTS = "file_access_tests"
private const val PLUGIN_SIGNAL_TESTS = "android_plugin_signal_tests"
private val LAUNCH_TESTS_SIGNAL = SignalInfo("launch_tests", String::class.java)
private val UPDATE_QUIT_ON_GO_BACK_SIGNAL = SignalInfo("update_quit_on_go_back", java.lang.Boolean::class.java)
private val SIGNALS = setOf(
LAUNCH_TESTS_SIGNAL
LAUNCH_TESTS_SIGNAL,
UPDATE_QUIT_ON_GO_BACK_SIGNAL
)
}
@@ -65,6 +70,8 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
// Add a countdown latch that is triggered when `onGodotMainLoopStarted` is fired.
// This will be used by tests to wait until the engine is ready.
latches[MAIN_LOOP_STARTED_LATCH_KEY] = CountDownLatch(1)
// Add a countdown latch that is triggered when the engine terminates.
latches[ENGINE_TERMINATING_LATCH_KEY] = CountDownLatch(1)
}
override fun getPluginName() = "GodotAppInstrumentedTestPlugin"
@@ -76,6 +83,11 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
latches.remove(MAIN_LOOP_STARTED_LATCH_KEY)?.countDown()
}
override fun onGodotTerminating() {
super.onGodotTerminating()
latches.remove(ENGINE_TERMINATING_LATCH_KEY)?.countDown()
}
/**
* Used by the instrumented test to wait until the Godot main loop is up and running.
*/
@@ -88,6 +100,19 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
}
}
internal fun waitForEngineTermination(timeoutInMs: Long) {
// Wait on the CountDownLatch for `onGodotTerminating`.
try {
latches[ENGINE_TERMINATING_LATCH_KEY]?.await(timeoutInMs, TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) {
Log.e(TAG, "Unable to wait for engine termination event.", e)
}
}
internal fun updateQuitOnGoBack(quitOnGoBack: Boolean) {
emitSignal(UPDATE_QUIT_ON_GO_BACK_SIGNAL, quitOnGoBack)
}
/**
* This launches the JavaClassWrapper tests, and wait until the tests are complete before returning.
*/
@@ -102,9 +127,13 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
return launchTests(FILE_ACCESS_TESTS)
}
internal fun runPluginSignalTests(): Result<Any>? {
return launchTests(PLUGIN_SIGNAL_TESTS)
}
private fun launchTests(testLabel: String): Result<Any>? {
val latch = latches.getOrPut(testLabel) { CountDownLatch(1) }
emitSignal(LAUNCH_TESTS_SIGNAL.name, testLabel)
emitSignal(LAUNCH_TESTS_SIGNAL, testLabel)
return try {
latch.await()
val result = testResults.remove(testLabel)
@@ -0,0 +1,35 @@
package com.godot.game.test
import android.util.Log
import org.godotengine.godot.Dictionary
import org.godotengine.godot.Godot
import org.godotengine.godot.plugin.GodotPlugin
import org.godotengine.godot.plugin.SignalInfo
import org.godotengine.godot.plugin.UsedByGodot
class SignalTestPlugin(godot: Godot) : GodotPlugin(godot) {
companion object {
private val EMISSION_TEST_SIGNAL = SignalInfo("emission_test_signal")
private val LAUNCH_TESTS_SIGNAL = SignalInfo("launch_tests", java.lang.Boolean::class.java, String::class.java)
}
override fun getPluginName() = "SignalTestPlugin"
override fun getPluginSignals(): Set<SignalInfo?> {
return setOf(
EMISSION_TEST_SIGNAL,
LAUNCH_TESTS_SIGNAL
)
}
@UsedByGodot
fun triggerTestSignal1() {
emitSignal(EMISSION_TEST_SIGNAL)
}
@UsedByGodot
fun triggerLaunchTestSignal() {
emitSignal(LAUNCH_TESTS_SIGNAL, true, "second message")
}
}
@@ -35,6 +35,7 @@
android:launchMode="singleInstancePerTask"
android:excludeFromRecents="false"
android:exported="false"
android:supportsPictureInPicture="true"
android:screenOrientation="landscape"
android:windowSoftInputMode="adjustResize"
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
@@ -67,9 +67,14 @@ public class GodotApp extends GodotActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
SplashScreen.installSplashScreen(this);
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
EdgeToEdge.enable(this);
super.onCreate(savedInstanceState);
Godot godot = getGodot();
if (godot != null && godot.getDisableGodotSplash()) {
splashScreen.setKeepOnScreenCondition(() -> godot.getRunStatus() != Godot.RunStatus.STARTED);
}
}
@Override
@@ -92,4 +97,9 @@ public class GodotApp extends GodotActivity {
super.onGodotForceQuit(instance);
}
}
@Override
protected boolean isPiPEnabled() {
return true;
}
}