[Android] Fix handling of back navigation when targeting API level 36
This commit is contained in:
+55
@@ -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
|
||||
@@ -169,4 +172,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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ func _ready():
|
||||
if Engine.has_singleton(_plugin_name):
|
||||
_android_plugin = Engine.get_singleton(_plugin_name)
|
||||
_android_plugin.connect("launch_tests", _launch_tests)
|
||||
_android_plugin.connect("update_quit_on_go_back", _update_quit_on_go_back)
|
||||
else:
|
||||
printerr("Couldn't find plugin " + _plugin_name)
|
||||
get_tree().quit()
|
||||
@@ -28,6 +29,10 @@ func _launch_tests(test_label: String) -> void:
|
||||
_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
-1
@@ -12,7 +12,7 @@ func run_tests():
|
||||
|
||||
# Scoped storage: Testing access to Downloads and Documents directory.
|
||||
var version = JavaClassWrapper.wrap("android.os.Build$VERSION")
|
||||
if version.SDK_INT >= 29:
|
||||
if version.SDK_INT >= 30:
|
||||
__exec_test(test_downloads_dir_access)
|
||||
__exec_test(test_documents_dir_access)
|
||||
|
||||
|
||||
+26
-2
@@ -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,17 @@ 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 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 +69,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 +82,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 +99,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.
|
||||
*/
|
||||
@@ -104,7 +128,7 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
|
||||
|
||||
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)
|
||||
|
||||
@@ -122,6 +122,15 @@ class Godot private constructor(val context: Context) {
|
||||
internal fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the engine current run status.
|
||||
*/
|
||||
enum class RunStatus {
|
||||
INITIALIZING,
|
||||
STARTED,
|
||||
TERMINATING
|
||||
}
|
||||
|
||||
private val mSensorManager: SensorManager? by lazy { context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager }
|
||||
private val mClipboard: ClipboardManager? by lazy { context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager }
|
||||
private val vibratorService: Vibrator? by lazy { context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator }
|
||||
@@ -185,9 +194,11 @@ class Godot private constructor(val context: Context) {
|
||||
private var resumed = false
|
||||
|
||||
/**
|
||||
* Tracks whether [onGodotSetupCompleted] fired.
|
||||
* Tracks the engine's run status.
|
||||
*/
|
||||
private val godotMainLoopStarted = AtomicBoolean(false)
|
||||
private val _runStatus = AtomicReference<RunStatus>(RunStatus.INITIALIZING)
|
||||
val runStatus: RunStatus
|
||||
get() = _runStatus.get()
|
||||
|
||||
val io = GodotIO(this)
|
||||
|
||||
@@ -697,7 +708,7 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
|
||||
private fun registerSensorsIfNeeded() {
|
||||
if (!resumed || !godotMainLoopStarted.get()) {
|
||||
if (!resumed || runStatus != RunStatus.STARTED) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -858,7 +869,7 @@ class Godot private constructor(val context: Context) {
|
||||
*/
|
||||
private fun onGodotMainLoopStarted() {
|
||||
Log.v(TAG, "OnGodotMainLoopStarted")
|
||||
godotMainLoopStarted.set(true)
|
||||
_runStatus.set(RunStatus.STARTED)
|
||||
|
||||
accelerometerEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_accelerometer")))
|
||||
gravityEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gravity")))
|
||||
@@ -881,6 +892,11 @@ class Godot private constructor(val context: Context) {
|
||||
@Keep
|
||||
private fun onGodotTerminating() {
|
||||
Log.v(TAG, "OnGodotTerminating")
|
||||
_runStatus.set(RunStatus.TERMINATING)
|
||||
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
plugin.onGodotTerminating()
|
||||
}
|
||||
runOnTerminate.get()?.run()
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.view.View
|
||||
import androidx.activity.addCallback
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
@@ -54,7 +55,7 @@ import java.util.concurrent.atomic.AtomicReference
|
||||
/**
|
||||
* Base abstract activity for Android apps intending to use Godot as the primary screen.
|
||||
*
|
||||
* Also a reference implementation for how to setup and use the [GodotFragment] fragment
|
||||
* Also a reference implementation for how to set up and use the [GodotFragment] fragment
|
||||
* within an Android app.
|
||||
*/
|
||||
abstract class GodotActivity : FragmentActivity(), GodotHost, PictureInPictureProvider {
|
||||
@@ -133,6 +134,9 @@ abstract class GodotActivity : FragmentActivity(), GodotHost, PictureInPicturePr
|
||||
|
||||
setContentView(getGodotAppLayout())
|
||||
|
||||
// Register `OnBackPressedCallback` for the Godot fragment.
|
||||
onBackPressedDispatcher.addCallback { godotFragment?.onBackPressed() }
|
||||
|
||||
handleStartIntent(intent, true)
|
||||
|
||||
val currentFragment = supportFragmentManager.findFragmentById(R.id.godot_fragment_container)
|
||||
@@ -283,10 +287,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost, PictureInPicturePr
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
godotFragment?.onBackPressed() ?: super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun getActivity(): Activity? {
|
||||
return this
|
||||
}
|
||||
|
||||
+6
-1
@@ -235,6 +235,11 @@ public abstract class GodotPlugin {
|
||||
*/
|
||||
public void onGodotMainLoopStarted() {}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when the Godot engine is terminating.
|
||||
*/
|
||||
public void onGodotTerminating() {}
|
||||
|
||||
/**
|
||||
* When using the OpenGL renderer, this is invoked once per frame on the GL thread after the
|
||||
* frame is drawn.
|
||||
@@ -414,7 +419,7 @@ public abstract class GodotPlugin {
|
||||
Object signalArg = signalArgs[i];
|
||||
if (signalArg != null && !signalParamTypes[i].isInstance(signalArg)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid type for argument #" + i + ". Should be of type " + signalParamTypes[i].getName());
|
||||
"Invalid type for argument #" + i + ". Should be of type '" + signalParamTypes[i].getName() + "' but is of type '" + signalArg.getClass().getName() + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user