Godot Android re-architecture

Decouples the Godot java entry point from the Android Fragment component. This enables the Godot component to be more easily reused across different types of Android components including Activities and Services.
This commit is contained in:
Fredia Huya-Kouadio 2023-05-22 15:40:38 -07:00
parent 00c782d959
commit 29bbc17b48
19 changed files with 1392 additions and 1168 deletions

View File

@ -30,7 +30,7 @@
package com.godot.game;
import org.godotengine.godot.FullScreenGodotApp;
import org.godotengine.godot.GodotActivity;
import android.os.Bundle;
@ -38,7 +38,7 @@ import android.os.Bundle;
* Template activity for Godot Android builds.
* Feel free to extend and modify this class for your custom logic.
*/
public class GodotApp extends FullScreenGodotApp {
public class GodotApp extends GodotActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
setTheme(R.style.GodotAppMainTheme);

View File

@ -39,7 +39,7 @@ import android.os.*
import android.util.Log
import android.widget.Toast
import androidx.window.layout.WindowMetricsCalculator
import org.godotengine.godot.FullScreenGodotApp
import org.godotengine.godot.GodotActivity
import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix
@ -55,7 +55,7 @@ import kotlin.math.min
*
* It also plays the role of the primary editor window.
*/
open class GodotEditor : FullScreenGodotApp() {
open class GodotEditor : GodotActivity() {
companion object {
private val TAG = GodotEditor::class.java.simpleName
@ -115,7 +115,7 @@ open class GodotEditor : FullScreenGodotApp() {
runOnUiThread {
// Enable long press, panning and scaling gestures
godotFragment?.renderView?.inputHandler?.apply {
godotFragment?.godot?.renderView?.inputHandler?.apply {
enableLongPress(longPressEnabled)
enablePanningAndScalingGestures(panScaleEnabled)
}
@ -318,7 +318,7 @@ open class GodotEditor : FullScreenGodotApp() {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)

View File

@ -30,156 +30,10 @@
package org.godotengine.godot;
import org.godotengine.godot.utils.ProcessPhoenix;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
/**
* Base activity for Android apps intending to use Godot as the primary and only screen.
* Base abstract activity for Android apps intending to use Godot as the primary screen.
*
* It's also a reference implementation for how to setup and use the {@link Godot} fragment
* within an Android app.
* @deprecated Use {@link GodotActivity}
*/
public abstract class FullScreenGodotApp extends FragmentActivity implements GodotHost {
private static final String TAG = FullScreenGodotApp.class.getSimpleName();
protected static final String EXTRA_FORCE_QUIT = "force_quit_requested";
protected static final String EXTRA_NEW_LAUNCH = "new_launch_requested";
@Nullable
private Godot godotFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.godot_app_layout);
handleStartIntent(getIntent(), true);
Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.godot_fragment_container);
if (currentFragment instanceof Godot) {
Log.v(TAG, "Reusing existing Godot fragment instance.");
godotFragment = (Godot)currentFragment;
} else {
Log.v(TAG, "Creating new Godot fragment instance.");
godotFragment = initGodotInstance();
getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss();
}
}
@Override
public void onDestroy() {
Log.v(TAG, "Destroying Godot app...");
super.onDestroy();
terminateGodotInstance(godotFragment);
}
@Override
public final void onGodotForceQuit(Godot instance) {
runOnUiThread(() -> {
terminateGodotInstance(instance);
});
}
private void terminateGodotInstance(Godot instance) {
if (instance == godotFragment) {
Log.v(TAG, "Force quitting Godot instance");
ProcessPhoenix.forceQuit(FullScreenGodotApp.this);
}
}
@Override
public final void onGodotRestartRequested(Godot instance) {
runOnUiThread(() -> {
if (instance == godotFragment) {
// It's very hard to properly de-initialize Godot on Android to restart the game
// from scratch. Therefore, we need to kill the whole app process and relaunch it.
//
// Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
// releasing and reloading native libs or resetting their state somehow and clearing static data).
Log.v(TAG, "Restarting Godot instance...");
ProcessPhoenix.triggerRebirth(FullScreenGodotApp.this);
}
});
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleStartIntent(intent, false);
if (godotFragment != null) {
godotFragment.onNewIntent(intent);
}
}
private void handleStartIntent(Intent intent, boolean newLaunch) {
boolean forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false);
if (forceQuitRequested) {
Log.d(TAG, "Force quit requested, terminating..");
ProcessPhoenix.forceQuit(this);
return;
}
if (!newLaunch) {
boolean newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false);
if (newLaunchRequested) {
Log.d(TAG, "New launch requested, restarting..");
Intent restartIntent = new Intent(intent).putExtra(EXTRA_NEW_LAUNCH, false);
ProcessPhoenix.triggerRebirth(this, restartIntent);
return;
}
}
}
@CallSuper
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (godotFragment != null) {
godotFragment.onActivityResult(requestCode, resultCode, data);
}
}
@CallSuper
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (godotFragment != null) {
godotFragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
@Override
public void onBackPressed() {
if (godotFragment != null) {
godotFragment.onBackPressed();
} else {
super.onBackPressed();
}
}
/**
* Used to initialize the Godot fragment instance in {@link FullScreenGodotApp#onCreate(Bundle)}.
*/
@NonNull
protected Godot initGodotInstance() {
return new Godot();
}
@Nullable
protected final Godot getGodotFragment() {
return godotFragment;
}
}
@Deprecated
public abstract class FullScreenGodotApp extends GodotActivity {}

View File

@ -0,0 +1,965 @@
/**************************************************************************/
/* Godot.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
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.*
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Rect
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.*
import android.util.Log
import android.view.*
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.FrameLayout
import androidx.annotation.Keep
import androidx.annotation.StringRes
import com.google.android.vending.expansion.downloader.*
import org.godotengine.godot.input.GodotEditText
import org.godotengine.godot.io.directory.DirectoryAccessHandler
import org.godotengine.godot.io.file.FileAccessHandler
import org.godotengine.godot.plugin.GodotPluginRegistry
import org.godotengine.godot.tts.GodotTTS
import org.godotengine.godot.utils.GodotNetUtils
import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.PermissionsUtil.requestPermission
import org.godotengine.godot.utils.beginBenchmarkMeasure
import org.godotengine.godot.utils.benchmarkFile
import org.godotengine.godot.utils.dumpBenchmark
import org.godotengine.godot.utils.endBenchmarkMeasure
import org.godotengine.godot.utils.useBenchmark
import org.godotengine.godot.xr.XRMode
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.util.*
/**
* Core component used to interface with the native layer of the engine.
*
* Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its
* lifecycle methods are properly invoked.
*/
class Godot(private val context: Context) : SensorEventListener {
private companion object {
private val TAG = Godot::class.java.simpleName
}
private val pluginRegistry: GodotPluginRegistry by lazy {
GodotPluginRegistry.initializePluginRegistry(this)
}
private val mSensorManager: SensorManager by lazy {
requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
private val mAccelerometer: Sensor by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
}
private val mGravity: Sensor by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
}
private val mMagnetometer: Sensor by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
}
private val mGyroscope: Sensor by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
}
private val mClipboard: ClipboardManager by lazy {
requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
}
private val uiChangeListener = View.OnSystemUiVisibilityChangeListener { visibility: Int ->
if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
val decorView = requireActivity().window.decorView
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
}}
val tts = GodotTTS(context)
val directoryAccessHandler = DirectoryAccessHandler(context)
val fileAccessHandler = FileAccessHandler(context)
val netUtils = GodotNetUtils(context)
/**
* Tracks whether [onCreate] was completed successfully.
*/
private var initializationStarted = false
/**
* Tracks whether [GodotLib.initialize] was completed successfully.
*/
private var nativeLayerInitializeCompleted = false
/**
* Tracks whether [GodotLib.setup] was completed successfully.
*/
private var nativeLayerSetupCompleted = false
/**
* Tracks whether [onInitRenderView] was completed successfully.
*/
private var renderViewInitialized = false
private var primaryHost: GodotHost? = null
var io: GodotIO? = null
private var commandLine : MutableList<String> = ArrayList<String>()
private var xrMode = XRMode.REGULAR
private var expansionPackPath: String = ""
private var useApkExpansion = false
private var useImmersive = false
private var useDebugOpengl = false
private var containerLayout: FrameLayout? = null
var renderView: GodotRenderView? = null
/**
* Returns true if the native engine has been initialized through [onInitNativeLayer], false otherwise.
*/
private fun isNativeInitialized() = nativeLayerInitializeCompleted && nativeLayerSetupCompleted
/**
* Returns true if the engine has been initialized, false otherwise.
*/
fun isInitialized() = initializationStarted && isNativeInitialized() && renderViewInitialized
/**
* Provides access to the primary host [Activity]
*/
fun getActivity() = primaryHost?.activity
private fun requireActivity() = getActivity() ?: throw IllegalStateException("Host activity must be non-null")
/**
* Start initialization of the Godot engine.
*
* This must be followed by [onInitNativeLayer] and [onInitRenderView] in that order to complete
* initialization of the engine.
*
* @throws IllegalArgumentException exception if the specified expansion pack (if any)
* is invalid.
*/
fun onCreate(primaryHost: GodotHost) {
if (this.primaryHost != null || initializationStarted) {
Log.d(TAG, "OnCreate already invoked")
return
}
beginBenchmarkMeasure("Godot::onCreate")
try {
this.primaryHost = primaryHost
val activity = requireActivity()
val window = activity.window
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
GodotPluginRegistry.initializePluginRegistry(this)
if (io == null) {
io = GodotIO(activity)
}
// check for apk expansion API
commandLine = getCommandLine()
var mainPackMd5: String? = null
var mainPackKey: String? = null
val newArgs: MutableList<String> = ArrayList()
var i = 0
while (i < commandLine.size) {
val hasExtra: Boolean = i < commandLine.size - 1
if (commandLine[i] == XRMode.REGULAR.cmdLineArg) {
xrMode = XRMode.REGULAR
} else if (commandLine[i] == XRMode.OPENXR.cmdLineArg) {
xrMode = XRMode.OPENXR
} else if (commandLine[i] == "--debug_opengl") {
useDebugOpengl = true
} else if (commandLine[i] == "--use_immersive") {
useImmersive = true
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or // hide nav bar
View.SYSTEM_UI_FLAG_FULLSCREEN or // hide status bar
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
registerUiChangeListener()
} else if (commandLine[i] == "--use_apk_expansion") {
useApkExpansion = true
} else if (hasExtra && commandLine[i] == "--apk_expansion_md5") {
mainPackMd5 = commandLine[i + 1]
i++
} else if (hasExtra && commandLine[i] == "--apk_expansion_key") {
mainPackKey = commandLine[i + 1]
val prefs = activity.getSharedPreferences(
"app_data_keys",
Context.MODE_PRIVATE
)
val editor = prefs.edit()
editor.putString("store_public_key", mainPackKey)
editor.apply()
i++
} else if (commandLine[i] == "--benchmark") {
useBenchmark = true
newArgs.add(commandLine[i])
} else if (hasExtra && commandLine[i] == "--benchmark-file") {
useBenchmark = true
newArgs.add(commandLine[i])
// Retrieve the filepath
benchmarkFile = commandLine[i + 1]
newArgs.add(commandLine[i + 1])
i++
} else if (commandLine[i].trim().isNotEmpty()) {
newArgs.add(commandLine[i])
}
i++
}
if (newArgs.isEmpty()) {
commandLine = mutableListOf()
} else {
commandLine = newArgs
}
if (useApkExpansion && mainPackMd5 != null && mainPackKey != null) {
// Build the full path to the app's expansion files
try {
expansionPackPath = Helpers.getSaveFilePath(context)
expansionPackPath += "/main." + activity.packageManager.getPackageInfo(
activity.packageName,
0
).versionCode + "." + activity.packageName + ".obb"
} catch (e: java.lang.Exception) {
Log.e(TAG, "Unable to build full path to the app's expansion files", e)
}
val f = File(expansionPackPath)
var packValid = true
if (!f.exists()) {
packValid = false
} else if (obbIsCorrupted(expansionPackPath, mainPackMd5)) {
packValid = false
try {
f.delete()
} catch (_: java.lang.Exception) {
}
}
if (!packValid) {
// Aborting engine initialization
throw IllegalArgumentException("Invalid expansion pack")
}
}
initializationStarted = true
} catch (e: java.lang.Exception) {
// Clear the primary host and rethrow
this.primaryHost = null
initializationStarted = false
throw e
} finally {
endBenchmarkMeasure("Godot::onCreate");
}
}
/**
* 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
}
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,
)
}
if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) {
nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts)
if (!nativeLayerSetupCompleted) {
Log.e(TAG, "Unable to setup the Godot engine! Aborting...")
alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit)
}
}
return isNativeInitialized()
}
/**
* Used to complete initialization of the view used by the engine for rendering.
*
* This must be preceded by [onCreate] and [onInitNativeLayer] in that order to properly
* initialize the engine.
*
* @param host The [GodotHost] that's initializing the render views
* @param providedContainerLayout Optional argument; if provided, this is reused to host the Godot's render views
*
* @return A [FrameLayout] instance containing Godot's render views if initialization is successful, null otherwise.
*
* @throws IllegalStateException if [onInitNativeLayer] has not been called
*/
@JvmOverloads
fun onInitRenderView(host: GodotHost, providedContainerLayout: FrameLayout = FrameLayout(host.activity)): FrameLayout? {
if (!isNativeInitialized()) {
throw IllegalStateException("onInitNativeLayer() must be invoked successfully prior to initializing the render view")
}
try {
val activity: Activity = host.activity
containerLayout = providedContainerLayout
containerLayout?.removeAllViews()
containerLayout?.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
// GodotEditText layout
val editText = GodotEditText(activity)
editText.layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
activity.resources.getDimension(R.dimen.text_edit_height).toInt()
)
// ...add to FrameLayout
containerLayout?.addView(editText)
renderView = if (usesVulkan()) {
if (!meetsVulkanRequirements(activity.packageManager)) {
alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit)
return null
}
GodotVulkanRenderView(host, this)
} else {
// Fallback to openGl
GodotGLRenderView(host, this, xrMode, useDebugOpengl)
}
if (host == primaryHost) {
renderView!!.startRenderer()
}
val view: View = renderView!!.view
containerLayout?.addView(
view,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
editText.setView(renderView)
io?.setEdit(editText)
// Listeners for keyboard height.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Report the height of virtual keyboard as it changes during the animation.
val decorView = activity.window.decorView
decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
var startBottom = 0
var endBottom = 0
override fun onPrepare(animation: WindowInsetsAnimation) {
startBottom = decorView.rootWindowInsets.getInsets(WindowInsets.Type.ime()).bottom
}
override fun onStart(animation: WindowInsetsAnimation, bounds: WindowInsetsAnimation.Bounds): WindowInsetsAnimation.Bounds {
endBottom = decorView.rootWindowInsets.getInsets(WindowInsets.Type.ime()).bottom
return bounds
}
override fun onProgress(windowInsets: WindowInsets, list: List<WindowInsetsAnimation>): WindowInsets {
// Find the IME animation.
var imeAnimation: WindowInsetsAnimation? = null
for (animation in list) {
if (animation.typeMask and WindowInsets.Type.ime() != 0) {
imeAnimation = animation
break
}
}
// Update keyboard height based on IME animation.
if (imeAnimation != null) {
val interpolatedFraction = imeAnimation.interpolatedFraction
// Linear interpolation between start and end values.
val keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction
GodotLib.setVirtualKeyboardHeight(keyboardHeight.toInt())
}
return windowInsets
}
override fun onEnd(animation: WindowInsetsAnimation) {}
})
} else {
// Infer the virtual keyboard height using visible area.
view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
// Don't allocate a new Rect every time the callback is called.
val visibleSize = Rect()
override fun onGlobalLayout() {
val surfaceView = renderView!!.view
surfaceView.getWindowVisibleDisplayFrame(visibleSize)
val keyboardHeight = surfaceView.height - visibleSize.bottom
GodotLib.setVirtualKeyboardHeight(keyboardHeight)
}
})
}
if (host == primaryHost) {
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) {
val pluginView = plugin.onMainCreate(activity)
if (pluginView != null) {
if (plugin.shouldBeOnTop()) {
containerLayout?.addView(pluginView)
} else {
containerLayout?.addView(pluginView, 0)
}
}
}
}
renderViewInitialized = true
} finally {
if (!renderViewInitialized) {
containerLayout?.removeAllViews()
containerLayout = null
}
}
return containerLayout
}
fun onResume(host: GodotHost) {
if (host != primaryHost) {
return
}
renderView!!.onActivityResumed()
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME)
mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME)
mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME)
if (useImmersive) {
val window = requireActivity().window
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or // hide nav bar
View.SYSTEM_UI_FLAG_FULLSCREEN or // hide status bar
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
}
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainResume()
}
}
fun onPause(host: GodotHost) {
if (host != primaryHost) {
return
}
renderView!!.onActivityPaused()
mSensorManager.unregisterListener(this)
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainPause()
}
}
fun onDestroy(primaryHost: GodotHost) {
if (this.primaryHost != primaryHost) {
return
}
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainDestroy()
}
GodotLib.ondestroy()
forceQuit()
}
/**
* Activity result callback
*/
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainActivityResult(requestCode, resultCode, data)
}
}
/**
* Permissions request callback
*/
fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults)
}
for (i in permissions.indices) {
GodotLib.requestPermissionResult(
permissions[i],
grantResults[i] == PackageManager.PERMISSION_GRANTED
)
}
}
/**
* Invoked on the render thread when the Godot setup is complete.
*/
private fun onGodotSetupCompleted() {
for (plugin in pluginRegistry.allPlugins) {
plugin.onGodotSetupCompleted()
}
primaryHost?.onGodotSetupCompleted()
}
/**
* Invoked on the render thread when the Godot main loop has started.
*/
private fun onGodotMainLoopStarted() {
for (plugin in pluginRegistry.allPlugins) {
plugin.onGodotMainLoopStarted()
}
primaryHost?.onGodotMainLoopStarted()
}
private fun restart() {
primaryHost?.onGodotRestartRequested(this)
}
private fun registerUiChangeListener() {
val decorView = requireActivity().window.decorView
decorView.setOnSystemUiVisibilityChangeListener(uiChangeListener)
}
@Keep
private fun alert(message: String, title: String) {
alert(message, title, null)
}
private fun alert(
@StringRes messageResId: Int,
@StringRes titleResId: Int,
okCallback: Runnable?
) {
val res: Resources = getActivity()?.resources ?: return
alert(res.getString(messageResId), res.getString(titleResId), okCallback)
}
private fun alert(message: String, title: String, okCallback: Runnable?) {
val activity: Activity = getActivity() ?: return
runOnUiThread(Runnable {
val builder = AlertDialog.Builder(activity)
builder.setMessage(message).setTitle(title)
builder.setPositiveButton(
"OK"
) { dialog: DialogInterface, id: Int ->
okCallback?.run()
dialog.cancel()
}
val dialog = builder.create()
dialog.show()
})
}
/**
* Queue a runnable to be run on the render thread.
*
* This must be called after the render thread has started.
*/
fun runOnRenderThread(action: Runnable) {
if (renderView != null) {
renderView!!.queueOnRenderThread(action)
}
}
/**
* Runs the specified action on the UI thread.
* If the current thread is the UI thread, then the action is executed immediately.
* If the current thread is not the UI thread, the action is posted to the event queue
* of the UI thread.
*/
fun runOnUiThread(action: Runnable) {
val activity: Activity = getActivity() ?: return
activity.runOnUiThread(action)
}
/**
* Returns true if the call is being made on the Ui thread.
*/
private fun isOnUiThread() = Looper.myLooper() == Looper.getMainLooper()
/**
* Returns true if `Vulkan` is used for rendering.
*/
private fun usesVulkan(): Boolean {
val renderer = GodotLib.getGlobal("rendering/renderer/rendering_method")
val renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver")
return ("forward_plus" == renderer || "mobile" == renderer) && "vulkan" == renderingDevice
}
/**
* Returns true if the device meets the base requirements for Vulkan support, false otherwise.
*/
private fun meetsVulkanRequirements(packageManager: PackageManager?): Boolean {
if (packageManager == null) {
return false
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) {
// Optional requirements.. log as warning if missing
Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1")
}
// Check for api version 1.0
return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003)
}
return false
}
private fun setKeepScreenOn(p_enabled: Boolean) {
runOnUiThread {
if (p_enabled) {
getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
getActivity()?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}
fun hasClipboard(): Boolean {
return mClipboard.hasPrimaryClip()
}
fun getClipboard(): String? {
val clipData = mClipboard.primaryClip ?: return ""
val text = clipData.getItemAt(0).text ?: return ""
return text.toString()
}
fun setClipboard(text: String?) {
val clip = ClipData.newPlainText("myLabel", text)
mClipboard.setPrimaryClip(clip)
}
private fun forceQuit() {
forceQuit(0)
}
@Keep
private fun forceQuit(instanceId: Int): Boolean {
if (primaryHost == null) {
return false
}
return if (instanceId == 0) {
primaryHost!!.onGodotForceQuit(this)
true
} else {
primaryHost!!.onGodotForceQuit(instanceId)
}
}
fun onBackPressed(host: GodotHost) {
if (host != primaryHost) {
return
}
var shouldQuit = true
for (plugin in pluginRegistry.allPlugins) {
if (plugin.onMainBackPressed()) {
shouldQuit = false
}
}
if (shouldQuit && renderView != null) {
renderView!!.queueOnRenderThread { GodotLib.back() }
}
}
private fun getRotatedValues(values: FloatArray?): FloatArray? {
if (values == null || values.size != 3) {
return values
}
val display =
(requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
val displayRotation = display.rotation
val rotatedValues = FloatArray(3)
when (displayRotation) {
Surface.ROTATION_0 -> {
rotatedValues[0] = values[0]
rotatedValues[1] = values[1]
rotatedValues[2] = values[2]
}
Surface.ROTATION_90 -> {
rotatedValues[0] = -values[1]
rotatedValues[1] = values[0]
rotatedValues[2] = values[2]
}
Surface.ROTATION_180 -> {
rotatedValues[0] = -values[0]
rotatedValues[1] = -values[1]
rotatedValues[2] = values[2]
}
Surface.ROTATION_270 -> {
rotatedValues[0] = values[1]
rotatedValues[1] = -values[0]
rotatedValues[2] = values[2]
}
}
return rotatedValues
}
override fun onSensorChanged(event: SensorEvent) {
if (renderView == null) {
return
}
when (event.sensor.type) {
Sensor.TYPE_ACCELEROMETER -> {
val rotatedValues = getRotatedValues(event.values)
renderView!!.queueOnRenderThread {
GodotLib.accelerometer(
-rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
)
}
}
Sensor.TYPE_GRAVITY -> {
val rotatedValues = getRotatedValues(event.values)
renderView!!.queueOnRenderThread {
GodotLib.gravity(
-rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
)
}
}
Sensor.TYPE_MAGNETIC_FIELD -> {
val rotatedValues = getRotatedValues(event.values)
renderView!!.queueOnRenderThread {
GodotLib.magnetometer(
-rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
)
}
}
Sensor.TYPE_GYROSCOPE -> {
val rotatedValues = getRotatedValues(event.values)
renderView!!.queueOnRenderThread {
GodotLib.gyroscope(
rotatedValues!![0], rotatedValues[1], rotatedValues[2]
)
}
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
// Do something here if sensor accuracy changes.
}
/**
* Used by the native code (java_godot_wrapper.h) to vibrate the device.
* @param durationMs
*/
@SuppressLint("MissingPermission")
@Keep
private fun vibrate(durationMs: Int) {
if (durationMs > 0 && requestPermission("VIBRATE")) {
val vibratorService = getActivity()?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibratorService.vibrate(
VibrationEffect.createOneShot(
durationMs.toLong(),
VibrationEffect.DEFAULT_AMPLITUDE
)
)
} else {
// deprecated in API 26
vibratorService.vibrate(durationMs.toLong())
}
}
}
private fun getCommandLine(): MutableList<String> {
val original: MutableList<String> = parseCommandLine()
val hostCommandLine = primaryHost?.commandLine
if (hostCommandLine != null && hostCommandLine.isNotEmpty()) {
original.addAll(hostCommandLine)
}
return original
}
private fun parseCommandLine(): MutableList<String> {
val inputStream: InputStream
return try {
inputStream = requireActivity().assets.open("_cl_")
val len = ByteArray(4)
var r = inputStream.read(len)
if (r < 4) {
return mutableListOf()
}
val argc =
(len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF)
val cmdline = ArrayList<String>(argc)
for (i in 0 until argc) {
r = inputStream.read(len)
if (r < 4) {
return mutableListOf()
}
val strlen =
(len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF)
if (strlen > 65535) {
return mutableListOf()
}
val arg = ByteArray(strlen)
r = inputStream.read(arg)
if (r == strlen) {
cmdline[i] = String(arg, StandardCharsets.UTF_8)
}
}
cmdline
} catch (e: Exception) {
// The _cl_ file can be missing with no adverse effect
mutableListOf()
}
}
/**
* 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.
*/
@Keep
private fun getInputFallbackMapping(): String? {
return xrMode.inputFallbackMapping
}
fun requestPermission(name: String?): Boolean {
return requestPermission(name, getActivity())
}
fun requestPermissions(): Boolean {
return PermissionsUtil.requestManifestPermissions(getActivity())
}
fun getGrantedPermissions(): Array<String?>? {
return PermissionsUtil.getGrantedPermissions(getActivity())
}
@Keep
private fun getCACertificates(): String {
return GodotNetUtils.getCACertificates()
}
private fun obbIsCorrupted(f: String, mainPackMd5: String): Boolean {
return try {
val fis: InputStream = FileInputStream(f)
// Create MD5 Hash
val buffer = ByteArray(16384)
val complete = MessageDigest.getInstance("MD5")
var numRead: Int
do {
numRead = fis.read(buffer)
if (numRead > 0) {
complete.update(buffer, 0, numRead)
}
} while (numRead != -1)
fis.close()
val messageDigest = complete.digest()
// Create Hex String
val hexString = StringBuilder()
for (b in messageDigest) {
var s = Integer.toHexString(0xFF and b.toInt())
if (s.length == 1) {
s = "0$s"
}
hexString.append(s)
}
val md5str = hexString.toString()
md5str != mainPackMd5
} catch (e: java.lang.Exception) {
e.printStackTrace()
true
}
}
@Keep
private fun initInputDevices() {
renderView!!.initInputDevices()
}
@Keep
private fun createNewGodotInstance(args: Array<String>): Int {
return primaryHost?.onNewGodotInstanceRequested(args) ?: 0
}
@Keep
private fun nativeBeginBenchmarkMeasure(label: String) {
beginBenchmarkMeasure(label)
}
@Keep
private fun nativeEndBenchmarkMeasure(label: String) {
endBenchmarkMeasure(label)
}
@Keep
private fun nativeDumpBenchmark(benchmarkFile: String) {
dumpBenchmark(fileAccessHandler, benchmarkFile)
}
}

View File

@ -0,0 +1,167 @@
/**************************************************************************/
/* GodotActivity.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
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.annotation.CallSuper
import androidx.fragment.app.FragmentActivity
import org.godotengine.godot.utils.ProcessPhoenix
/**
* 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
* within an Android app.
*/
abstract class GodotActivity : FragmentActivity(), GodotHost {
companion object {
private val TAG = GodotActivity::class.java.simpleName
@JvmStatic
protected val EXTRA_FORCE_QUIT = "force_quit_requested"
@JvmStatic
protected val EXTRA_NEW_LAUNCH = "new_launch_requested"
}
/**
* Interaction with the [Godot] object is delegated to the [GodotFragment] class.
*/
protected var godotFragment: GodotFragment? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.godot_app_layout)
handleStartIntent(intent, true)
val currentFragment = supportFragmentManager.findFragmentById(R.id.godot_fragment_container)
if (currentFragment is GodotFragment) {
Log.v(TAG, "Reusing existing Godot fragment instance.")
godotFragment = currentFragment
} else {
Log.v(TAG, "Creating new Godot fragment instance.")
godotFragment = initGodotInstance()
supportFragmentManager.beginTransaction().replace(R.id.godot_fragment_container, godotFragment!!).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss()
}
}
override fun onDestroy() {
Log.v(TAG, "Destroying Godot app...")
super.onDestroy()
if (godotFragment != null) {
terminateGodotInstance(godotFragment!!.godot)
}
}
override fun onGodotForceQuit(instance: Godot) {
runOnUiThread { terminateGodotInstance(instance) }
}
private fun terminateGodotInstance(instance: Godot) {
if (godotFragment != null && instance === godotFragment!!.godot) {
Log.v(TAG, "Force quitting Godot instance")
ProcessPhoenix.forceQuit(this)
}
}
override fun onGodotRestartRequested(instance: Godot) {
runOnUiThread {
if (godotFragment != null && instance === godotFragment!!.godot) {
// It's very hard to properly de-initialize Godot on Android to restart the game
// from scratch. Therefore, we need to kill the whole app process and relaunch it.
//
// Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
// releasing and reloading native libs or resetting their state somehow and clearing static data).
Log.v(TAG, "Restarting Godot instance...")
ProcessPhoenix.triggerRebirth(this)
}
}
}
override fun onNewIntent(newIntent: Intent) {
super.onNewIntent(newIntent)
intent = newIntent
handleStartIntent(newIntent, false)
godotFragment?.onNewIntent(newIntent)
}
private fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
val forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false)
if (forceQuitRequested) {
Log.d(TAG, "Force quit requested, terminating..")
ProcessPhoenix.forceQuit(this)
return
}
if (!newLaunch) {
val newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false)
if (newLaunchRequested) {
Log.d(TAG, "New launch requested, restarting..")
val restartIntent = Intent(intent).putExtra(EXTRA_NEW_LAUNCH, false)
ProcessPhoenix.triggerRebirth(this, restartIntent)
return
}
}
}
@CallSuper
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
godotFragment?.onActivityResult(requestCode, resultCode, data)
}
@CallSuper
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
godotFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onBackPressed() {
godotFragment?.onBackPressed() ?: super.onBackPressed()
}
override fun getActivity(): Activity? {
return this
}
/**
* Used to initialize the Godot fragment instance in [onCreate].
*/
protected open fun initGodotInstance(): GodotFragment {
return GodotFragment()
}
}

View File

@ -29,10 +29,10 @@
/**************************************************************************/
package org.godotengine.godot;
import org.godotengine.godot.gl.GLSurfaceView;
import org.godotengine.godot.gl.GodotRenderer;
import org.godotengine.godot.input.GodotInputHandler;
import org.godotengine.godot.utils.GLUtils;
import org.godotengine.godot.xr.XRMode;
import org.godotengine.godot.xr.ovr.OvrConfigChooser;
import org.godotengine.godot.xr.ovr.OvrContextFactory;
@ -78,22 +78,23 @@ import java.io.InputStream;
* bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
*/
public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
private final GodotHost host;
private final Godot godot;
private final GodotInputHandler inputHandler;
private final GodotRenderer godotRenderer;
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
public GodotGLRenderView(Context context, Godot godot, XRMode xrMode, boolean p_use_debug_opengl) {
super(context);
GLUtils.use_debug_opengl = p_use_debug_opengl;
public GodotGLRenderView(GodotHost host, Godot godot, XRMode xrMode, boolean useDebugOpengl) {
super(host.getActivity());
this.host = host;
this.godot = godot;
this.inputHandler = new GodotInputHandler(this);
this.godotRenderer = new GodotRenderer();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
}
init(xrMode, false);
init(xrMode, false, useDebugOpengl);
}
@Override
@ -123,7 +124,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
@Override
public void onBackPressed() {
godot.onBackPressed();
godot.onBackPressed(host);
}
@Override
@ -233,7 +234,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
return super.onResolvePointerIcon(me, pointerIndex);
}
private void init(XRMode xrMode, boolean translucent) {
private void init(XRMode xrMode, boolean translucent, boolean useDebugOpengl) {
setPreserveEGLContextOnPause(true);
setFocusableInTouchMode(true);
switch (xrMode) {
@ -262,7 +263,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
/* Setup the context factory for 2.0 rendering.
* See ContextFactory class definition below
*/
setEGLContextFactory(new RegularContextFactory());
setEGLContextFactory(new RegularContextFactory(useDebugOpengl));
/* We need to choose an EGLConfig that matches the format of
* our surface exactly. This is going to be done in our
@ -275,7 +276,10 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
new RegularConfigChooser(8, 8, 8, 8, 16, 0)));
break;
}
}
@Override
public void startRenderer() {
/* Set the renderer responsible for frame rendering */
setRenderer(godotRenderer);
}

View File

@ -30,11 +30,13 @@
package org.godotengine.godot;
import android.app.Activity;
import java.util.Collections;
import java.util.List;
/**
* Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} fragment.
* Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} engine.
*/
public interface GodotHost {
/**
@ -86,4 +88,9 @@ public interface GodotHost {
default int onNewGodotInstanceRequested(String[] args) {
return 0;
}
/**
* Provide access to the Activity hosting the Godot engine.
*/
Activity getActivity();
}

View File

@ -39,6 +39,11 @@ public interface GodotRenderView {
void initInputDevices();
/**
* Starts the thread that will drive Godot's rendering.
*/
void startRenderer();
void queueOnRenderThread(Runnable event);
void onActivityPaused();

View File

@ -0,0 +1,54 @@
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.
*/
class GodotService : Service() {
companion object {
private val TAG = GodotService::class.java.simpleName
}
private var boundIntent: Intent? = null
private val godot by lazy {
Godot(applicationContext)
}
override fun onCreate() {
super.onCreate()
}
override fun onDestroy() {
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
if (boundIntent != null) {
Log.d(TAG, "GodotService already bound")
return null
}
boundIntent = intent
return GodotHandle(godot)
}
override fun onRebind(intent: Intent?) {
super.onRebind(intent)
}
override fun onUnbind(intent: Intent?): Boolean {
return super.onUnbind(intent)
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
}
class GodotHandle(val godot: Godot) : Binder()
}

View File

@ -35,7 +35,6 @@ import org.godotengine.godot.vulkan.VkRenderer;
import org.godotengine.godot.vulkan.VkSurfaceView;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@ -52,14 +51,16 @@ import androidx.annotation.Keep;
import java.io.InputStream;
public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
private final GodotHost host;
private final Godot godot;
private final GodotInputHandler mInputHandler;
private final VkRenderer mRenderer;
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
public GodotVulkanRenderView(Context context, Godot godot) {
super(context);
public GodotVulkanRenderView(GodotHost host, Godot godot) {
super(host.getActivity());
this.host = host;
this.godot = godot;
mInputHandler = new GodotInputHandler(this);
mRenderer = new VkRenderer();
@ -67,6 +68,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
}
setFocusableInTouchMode(true);
}
@Override
public void startRenderer() {
startRenderer(mRenderer);
}
@ -97,7 +102,7 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
@Override
public void onBackPressed() {
godot.onBackPressed();
godot.onBackPressed(host);
}
@Override

View File

@ -33,6 +33,7 @@ package org.godotengine.godot.tts;
import org.godotengine.godot.GodotLib;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
@ -62,7 +63,7 @@ public class GodotTTS extends UtteranceProgressListener {
final private static int EVENT_CANCEL = 2;
final private static int EVENT_BOUNDARY = 3;
final private Activity activity;
private final Context context;
private TextToSpeech synth;
private LinkedList<GodotUtterance> queue;
final private Object lock = new Object();
@ -71,8 +72,8 @@ public class GodotTTS extends UtteranceProgressListener {
private boolean speaking;
private boolean paused;
public GodotTTS(Activity p_activity) {
activity = p_activity;
public GodotTTS(Context context) {
this.context = context;
}
private void updateTTS() {
@ -188,7 +189,7 @@ public class GodotTTS extends UtteranceProgressListener {
* Initialize synth and query.
*/
public void init() {
synth = new TextToSpeech(activity, null);
synth = new TextToSpeech(context, null);
queue = new LinkedList<GodotUtterance>();
synth.setOnUtteranceProgressListener(this);

View File

@ -44,8 +44,6 @@ public class GLUtils {
public static final boolean DEBUG = false;
public static boolean use_debug_opengl = false;
private static final String[] ATTRIBUTES_NAMES = new String[] {
"EGL_BUFFER_SIZE",
"EGL_ALPHA_SIZE",

View File

@ -36,7 +36,8 @@ import android.net.wifi.WifiManager;
import android.util.Base64;
import android.util.Log;
import java.io.StringWriter;
import androidx.annotation.NonNull;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
@ -50,9 +51,9 @@ public class GodotNetUtils {
/* A single, reference counted, multicast lock, or null if permission CHANGE_WIFI_MULTICAST_STATE is missing */
private WifiManager.MulticastLock multicastLock;
public GodotNetUtils(Activity p_activity) {
if (PermissionsUtil.hasManifestPermission(p_activity, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) {
WifiManager wifi = (WifiManager)p_activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
public GodotNetUtils(Context context) {
if (PermissionsUtil.hasManifestPermission(context, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) {
WifiManager wifi = (WifiManager)context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
multicastLock = wifi.createMulticastLock("GodotMulticastLock");
multicastLock.setReferenceCounted(true);
}
@ -91,7 +92,7 @@ public class GodotNetUtils {
* @see https://developer.android.com/reference/java/security/KeyStore .
* @return A string of concatenated X509 certificates in PEM format.
*/
public static String getCACertificates() {
public static @NonNull String getCACertificates() {
try {
KeyStore ks = KeyStore.getInstance("AndroidCAStore");
StringBuilder writer = new StringBuilder();

View File

@ -32,6 +32,7 @@ package org.godotengine.godot.utils;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@ -52,7 +53,6 @@ import java.util.Set;
/**
* This class includes utility functions for Android permissions related operations.
*/
public final class PermissionsUtil {
private static final String TAG = PermissionsUtil.class.getSimpleName();
@ -193,13 +193,13 @@ public final class PermissionsUtil {
/**
* With this function you can get the list of dangerous permissions that have been granted to the Android application.
* @param activity the caller activity for this method.
* @param context the caller context for this method.
* @return granted permissions list
*/
public static String[] getGrantedPermissions(Activity activity) {
public static String[] getGrantedPermissions(Context context) {
String[] manifestPermissions;
try {
manifestPermissions = getManifestPermissions(activity);
manifestPermissions = getManifestPermissions(context);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return new String[0];
@ -215,9 +215,9 @@ public final class PermissionsUtil {
grantedPermissions.add(manifestPermission);
}
} else {
PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
PermissionInfo permissionInfo = getPermissionInfo(context, manifestPermission);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
grantedPermissions.add(manifestPermission);
}
}
@ -232,13 +232,13 @@ public final class PermissionsUtil {
/**
* Check if the given permission is in the AndroidManifest.xml file.
* @param activity the caller activity for this method.
* @param context the caller context for this method.
* @param permission the permession to look for in the manifest file.
* @return "true" if the permission is in the manifest file of the activity, "false" otherwise.
*/
public static boolean hasManifestPermission(Activity activity, String permission) {
public static boolean hasManifestPermission(Context context, String permission) {
try {
for (String p : getManifestPermissions(activity)) {
for (String p : getManifestPermissions(context)) {
if (permission.equals(p))
return true;
}
@ -250,13 +250,13 @@ public final class PermissionsUtil {
/**
* Returns the permissions defined in the AndroidManifest.xml file.
* @param activity the caller activity for this method.
* @param context the caller context for this method.
* @return manifest permissions list
* @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found.
*/
private static String[] getManifestPermissions(Activity activity) throws PackageManager.NameNotFoundException {
PackageManager packageManager = activity.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(activity.getPackageName(), PackageManager.GET_PERMISSIONS);
private static String[] getManifestPermissions(Context context) throws PackageManager.NameNotFoundException {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
if (packageInfo.requestedPermissions == null)
return new String[0];
return packageInfo.requestedPermissions;
@ -264,13 +264,13 @@ public final class PermissionsUtil {
/**
* Returns the information of the desired permission.
* @param activity the caller activity for this method.
* @param context the caller context for this method.
* @param permission the name of the permission.
* @return permission info object
* @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found.
*/
private static PermissionInfo getPermissionInfo(Activity activity, String permission) throws PackageManager.NameNotFoundException {
PackageManager packageManager = activity.getPackageManager();
private static PermissionInfo getPermissionInfo(Context context, String permission) throws PackageManager.NameNotFoundException {
PackageManager packageManager = context.getPackageManager();
return packageManager.getPermissionInfo(permission, 0);
}
}

View File

@ -51,12 +51,22 @@ public class RegularContextFactory implements GLSurfaceView.EGLContextFactory {
private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
private final boolean mUseDebugOpengl;
public RegularContextFactory() {
this(false);
}
public RegularContextFactory(boolean useDebugOpengl) {
this.mUseDebugOpengl = useDebugOpengl;
}
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
Log.w(TAG, "creating OpenGL ES 3.0 context :");
GLUtils.checkEglError(TAG, "Before eglCreateContext", egl);
EGLContext context;
if (GLUtils.use_debug_opengl) {
if (mUseDebugOpengl) {
int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE };
context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
} else {

View File

@ -135,7 +135,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv
os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion);
return godot_java->on_video_init(env);
return true;
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz) {

View File

@ -58,12 +58,10 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
}
// get some Godot method pointers...
_on_video_init = p_env->GetMethodID(godot_class, "onVideoInit", "()Z");
_restart = p_env->GetMethodID(godot_class, "restart", "()V");
_finish = p_env->GetMethodID(godot_class, "forceQuit", "(I)Z");
_set_keep_screen_on = p_env->GetMethodID(godot_class, "setKeepScreenOn", "(Z)V");
_alert = p_env->GetMethodID(godot_class, "alert", "(Ljava/lang/String;Ljava/lang/String;)V");
_get_GLES_version_code = p_env->GetMethodID(godot_class, "getGLESVersionCode", "()I");
_get_clipboard = p_env->GetMethodID(godot_class, "getClipboard", "()Ljava/lang/String;");
_set_clipboard = p_env->GetMethodID(godot_class, "setClipboard", "(Ljava/lang/String;)V");
_has_clipboard = p_env->GetMethodID(godot_class, "hasClipboard", "()Z");
@ -72,20 +70,15 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;");
_get_ca_certificates = p_env->GetMethodID(godot_class, "getCACertificates", "()Ljava/lang/String;");
_init_input_devices = p_env->GetMethodID(godot_class, "initInputDevices", "()V");
_get_surface = p_env->GetMethodID(godot_class, "getSurface", "()Landroid/view/Surface;");
_is_activity_resumed = p_env->GetMethodID(godot_class, "isActivityResumed", "()Z");
_vibrate = p_env->GetMethodID(godot_class, "vibrate", "(I)V");
_get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;");
_on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V");
_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
_create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I");
_get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;");
_begin_benchmark_measure = p_env->GetMethodID(godot_class, "beginBenchmarkMeasure", "(Ljava/lang/String;)V");
_end_benchmark_measure = p_env->GetMethodID(godot_class, "endBenchmarkMeasure", "(Ljava/lang/String;)V");
_dump_benchmark = p_env->GetMethodID(godot_class, "dumpBenchmark", "(Ljava/lang/String;)V");
// get some Activity method pointers...
_get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;");
_begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;)V");
_end_benchmark_measure = p_env->GetMethodID(godot_class, "nativeEndBenchmarkMeasure", "(Ljava/lang/String;)V");
_dump_benchmark = p_env->GetMethodID(godot_class, "nativeDumpBenchmark", "(Ljava/lang/String;)V");
}
GodotJavaWrapper::~GodotJavaWrapper() {
@ -105,29 +98,6 @@ jobject GodotJavaWrapper::get_activity() {
return activity;
}
jobject GodotJavaWrapper::get_member_object(const char *p_name, const char *p_class, JNIEnv *p_env) {
if (godot_class) {
if (p_env == nullptr) {
p_env = get_jni_env();
}
ERR_FAIL_NULL_V(p_env, nullptr);
jfieldID fid = p_env->GetStaticFieldID(godot_class, p_name, p_class);
return p_env->GetStaticObjectField(godot_class, fid);
} else {
return nullptr;
}
}
jobject GodotJavaWrapper::get_class_loader() {
if (_get_class_loader) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, nullptr);
return env->CallObjectMethod(activity, _get_class_loader);
} else {
return nullptr;
}
}
GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() {
if (godot_view != nullptr) {
return godot_view;
@ -143,17 +113,6 @@ GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() {
return godot_view;
}
bool GodotJavaWrapper::on_video_init(JNIEnv *p_env) {
if (_on_video_init) {
if (p_env == nullptr) {
p_env = get_jni_env();
}
ERR_FAIL_NULL_V(p_env, false);
return p_env->CallBooleanMethod(godot_instance, _on_video_init);
}
return false;
}
void GodotJavaWrapper::on_godot_setup_completed(JNIEnv *p_env) {
if (_on_godot_setup_completed) {
if (p_env == nullptr) {
@ -212,15 +171,6 @@ void GodotJavaWrapper::alert(const String &p_message, const String &p_title) {
}
}
int GodotJavaWrapper::get_gles_version_code() {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, 0);
if (_get_GLES_version_code) {
return env->CallIntMethod(godot_instance, _get_GLES_version_code);
}
return 0;
}
bool GodotJavaWrapper::has_get_clipboard() {
return _get_clipboard != nullptr;
}
@ -333,26 +283,6 @@ void GodotJavaWrapper::init_input_devices() {
}
}
jobject GodotJavaWrapper::get_surface() {
if (_get_surface) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, nullptr);
return env->CallObjectMethod(godot_instance, _get_surface);
} else {
return nullptr;
}
}
bool GodotJavaWrapper::is_activity_resumed() {
if (_is_activity_resumed) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, false);
return env->CallBooleanMethod(godot_instance, _is_activity_resumed);
} else {
return false;
}
}
void GodotJavaWrapper::vibrate(int p_duration_ms) {
if (_vibrate) {
JNIEnv *env = get_jni_env();

View File

@ -49,12 +49,10 @@ private:
GodotJavaViewWrapper *godot_view = nullptr;
jmethodID _on_video_init = nullptr;
jmethodID _restart = nullptr;
jmethodID _finish = nullptr;
jmethodID _set_keep_screen_on = nullptr;
jmethodID _alert = nullptr;
jmethodID _get_GLES_version_code = nullptr;
jmethodID _get_clipboard = nullptr;
jmethodID _set_clipboard = nullptr;
jmethodID _has_clipboard = nullptr;
@ -63,13 +61,10 @@ private:
jmethodID _get_granted_permissions = nullptr;
jmethodID _get_ca_certificates = nullptr;
jmethodID _init_input_devices = nullptr;
jmethodID _get_surface = nullptr;
jmethodID _is_activity_resumed = nullptr;
jmethodID _vibrate = nullptr;
jmethodID _get_input_fallback_mapping = nullptr;
jmethodID _on_godot_setup_completed = nullptr;
jmethodID _on_godot_main_loop_started = nullptr;
jmethodID _get_class_loader = nullptr;
jmethodID _create_new_godot_instance = nullptr;
jmethodID _get_render_view = nullptr;
jmethodID _begin_benchmark_measure = nullptr;
@ -81,19 +76,15 @@ public:
~GodotJavaWrapper();
jobject get_activity();
jobject get_member_object(const char *p_name, const char *p_class, JNIEnv *p_env = nullptr);
jobject get_class_loader();
GodotJavaViewWrapper *get_godot_view();
bool on_video_init(JNIEnv *p_env = nullptr);
void on_godot_setup_completed(JNIEnv *p_env = nullptr);
void on_godot_main_loop_started(JNIEnv *p_env = nullptr);
void restart(JNIEnv *p_env = nullptr);
bool force_quit(JNIEnv *p_env = nullptr, int p_instance_id = 0);
void set_keep_screen_on(bool p_enabled);
void alert(const String &p_message, const String &p_title);
int get_gles_version_code();
bool has_get_clipboard();
String get_clipboard();
bool has_set_clipboard();
@ -105,8 +96,6 @@ public:
Vector<String> get_granted_permissions() const;
String get_ca_certificates() const;
void init_input_devices();
jobject get_surface();
bool is_activity_resumed();
void vibrate(int p_duration_ms);
String get_input_fallback_mapping();
int create_new_godot_instance(List<String> args);