feat: updated engine version to 4.4-rc1
This commit is contained in:
parent
ee00efde1f
commit
21ba8e33af
5459 changed files with 1128836 additions and 198305 deletions
|
|
@ -1,39 +0,0 @@
|
|||
# Third-party libraries
|
||||
|
||||
This file list third-party libraries used in the Android source folder,
|
||||
with their provenance and, when relevant, modifications made to those files.
|
||||
|
||||
## com.android.vending.billing
|
||||
|
||||
- Upstream: https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive/app/src/main
|
||||
- Version: git (7a94c69, 2019)
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`.
|
||||
|
||||
## com.google.android.vending.expansion.downloader
|
||||
|
||||
- Upstream: https://github.com/google/play-apk-expansion/tree/master/apkx_library
|
||||
- Version: git (9ecf54e, 2017)
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite all files under:
|
||||
|
||||
- `src/com/google/android/vending/expansion/downloader`
|
||||
|
||||
Some files have been modified for yet unclear reasons.
|
||||
See the `patches/com.google.android.vending.expansion.downloader.patch` file.
|
||||
|
||||
## com.google.android.vending.licensing
|
||||
|
||||
- Upstream: https://github.com/google/play-licensing/tree/master/lvl_library/
|
||||
- Version: git (eb57657, 2018) with modifications
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite all files under:
|
||||
|
||||
- `aidl/com/android/vending/licensing`
|
||||
- `src/com/google/android/vending/licensing`
|
||||
|
||||
Some files have been modified to silence linter errors or fix downstream issues.
|
||||
See the `patches/com.google.android.vending.licensing.patch` file.
|
||||
|
|
@ -51,7 +51,7 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
flavorDimensions "products"
|
||||
flavorDimensions = ["products"]
|
||||
productFlavors {
|
||||
editor {}
|
||||
template {}
|
||||
|
|
@ -104,10 +104,10 @@ android {
|
|||
}
|
||||
|
||||
boolean devBuild = buildType == "dev"
|
||||
boolean debugSymbols = devBuild || isAndroidStudio()
|
||||
boolean debugSymbols = devBuild
|
||||
boolean runTests = devBuild
|
||||
boolean productionBuild = !devBuild
|
||||
boolean storeRelease = buildType == "release"
|
||||
boolean productionBuild = storeRelease
|
||||
|
||||
def sconsTarget = flavorName
|
||||
if (sconsTarget == "template") {
|
||||
|
|
|
|||
27
engine/platform/android/java/lib/res/layout/snackbar.xml
Normal file
27
engine/platform/android/java/lib/res/layout/snackbar.xml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:drawable/dialog_holo_dark_frame"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/snackbar_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text=""
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:padding="8dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/snackbar_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#00FFFFFF"
|
||||
android:text="Action"
|
||||
android:textColor="#61B7FC"
|
||||
android:paddingHorizontal="8dp"/>
|
||||
</LinearLayout>
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
WARNING: The content of this file must always match the constant 'platform/android/export/export_plugin.cpp#ICON_XML_TEMPLATE'.
|
||||
-->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/icon_background"/>
|
||||
<foreground android:drawable="@mipmap/icon_foreground"/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
This file is created to work alongside the icon.xml file.
|
||||
If the user provides a Monochrome icon in the export settings, its data will be used to overwrite the icon.xml file.
|
||||
We needed to create this file to get a reference for icon_monochrome.
|
||||
-->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/icon_background"/>
|
||||
<foreground android:drawable="@mipmap/icon_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/icon_monochrome"/>
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
BIN
engine/platform/android/java/lib/res/mipmap/icon_monochrome.png
Normal file
BIN
engine/platform/android/java/lib/res/mipmap/icon_monochrome.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,4 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="text_edit_height">48dp</dimen>
|
||||
<dimen name="button_height">48dp</dimen>
|
||||
<dimen name="button_padding">10dp</dimen>
|
||||
<dimen name="dialog_padding_horizontal">16dp</dimen>
|
||||
<dimen name="dialog_padding_vertical">8dp</dimen>
|
||||
<dimen name="snackbar_bottom_margin">10dp</dimen>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -55,4 +55,7 @@
|
|||
<string name="kilobytes_per_second">%1$s KB/s</string>
|
||||
<string name="time_remaining">Time remaining: %1$s</string>
|
||||
<string name="time_remaining_notification">%1$s left</string>
|
||||
|
||||
<!-- Labels for the dialog action buttons -->
|
||||
<string name="dialog_ok">OK</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -39,25 +39,32 @@ import android.content.res.Configuration
|
|||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
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.util.TypedValue
|
||||
import android.view.*
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.Keep
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsAnimationCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.google.android.vending.expansion.downloader.*
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.input.GodotEditText
|
||||
import org.godotengine.godot.input.GodotInputHandler
|
||||
import org.godotengine.godot.io.FilePicker
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler
|
||||
import org.godotengine.godot.io.file.FileAccessHandler
|
||||
import org.godotengine.godot.plugin.AndroidRuntimePlugin
|
||||
import org.godotengine.godot.plugin.GodotPlugin
|
||||
import org.godotengine.godot.plugin.GodotPluginRegistry
|
||||
import org.godotengine.godot.tts.GodotTTS
|
||||
import org.godotengine.godot.utils.CommandLineFileParser
|
||||
import org.godotengine.godot.utils.DialogUtils
|
||||
import org.godotengine.godot.utils.GodotNetUtils
|
||||
import org.godotengine.godot.utils.PermissionsUtil
|
||||
import org.godotengine.godot.utils.PermissionsUtil.requestPermission
|
||||
|
|
@ -73,25 +80,31 @@ import java.io.InputStream
|
|||
import java.lang.Exception
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
class Godot(private val context: Context) {
|
||||
|
||||
private companion object {
|
||||
internal companion object {
|
||||
private val TAG = Godot::class.java.simpleName
|
||||
|
||||
// Supported build flavors
|
||||
const val EDITOR_FLAVOR = "editor"
|
||||
const val TEMPLATE_FLAVOR = "template"
|
||||
|
||||
/**
|
||||
* @return true if this is an editor build, false if this is a template build
|
||||
*/
|
||||
fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
|
||||
}
|
||||
|
||||
private val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
private val mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
|
@ -99,34 +112,33 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
private val pluginRegistry: GodotPluginRegistry by lazy {
|
||||
GodotPluginRegistry.getPluginRegistry()
|
||||
}
|
||||
|
||||
private val accelerometerEnabled = AtomicBoolean(false)
|
||||
private val mAccelerometer: Sensor? by lazy {
|
||||
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||
}
|
||||
|
||||
private val gravityEnabled = AtomicBoolean(false)
|
||||
private val mGravity: Sensor? by lazy {
|
||||
mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
|
||||
}
|
||||
|
||||
private val magnetometerEnabled = AtomicBoolean(false)
|
||||
private val mMagnetometer: Sensor? by lazy {
|
||||
mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
||||
}
|
||||
|
||||
private val gyroscopeEnabled = AtomicBoolean(false)
|
||||
private val mGyroscope: Sensor? by lazy {
|
||||
mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
||||
}
|
||||
|
||||
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)
|
||||
private val commandLineFileParser = CommandLineFileParser()
|
||||
private val godotInputHandler = GodotInputHandler(context, this)
|
||||
|
||||
/**
|
||||
* Task to run when the engine terminates.
|
||||
|
|
@ -154,13 +166,24 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
private var renderViewInitialized = false
|
||||
private var primaryHost: GodotHost? = null
|
||||
|
||||
/**
|
||||
* Tracks whether we're in the RESUMED lifecycle state.
|
||||
* See [onResume] and [onPause]
|
||||
*/
|
||||
private var resumed = false
|
||||
|
||||
/**
|
||||
* Tracks whether [onGodotSetupCompleted] fired.
|
||||
*/
|
||||
private val godotMainLoopStarted = AtomicBoolean(false)
|
||||
|
||||
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 val useImmersive = AtomicBoolean(false)
|
||||
private var useDebugOpengl = false
|
||||
private var darkMode = false
|
||||
|
||||
|
|
@ -210,7 +233,9 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
|
||||
|
||||
Log.v(TAG, "Initializing Godot plugin registry")
|
||||
GodotPluginRegistry.initializePluginRegistry(this, primaryHost.getHostPlugins(this))
|
||||
val runtimePlugins = mutableSetOf<GodotPlugin>(AndroidRuntimePlugin(this))
|
||||
runtimePlugins.addAll(primaryHost.getHostPlugins(this))
|
||||
GodotPluginRegistry.initializePluginRegistry(this, runtimePlugins)
|
||||
if (io == null) {
|
||||
io = GodotIO(activity)
|
||||
}
|
||||
|
|
@ -229,15 +254,9 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
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] == "--fullscreen") {
|
||||
useImmersive.set(true)
|
||||
newArgs.add(commandLine[i])
|
||||
} else if (commandLine[i] == "--use_apk_expansion") {
|
||||
useApkExpansion = true
|
||||
} else if (hasExtra && commandLine[i] == "--apk_expansion_md5") {
|
||||
|
|
@ -310,6 +329,54 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle immersive mode.
|
||||
* Must be called from the UI thread.
|
||||
*/
|
||||
fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) {
|
||||
val activity = getActivity() ?: return
|
||||
val window = activity.window ?: return
|
||||
|
||||
if (!useImmersive.compareAndSet(!enabled, enabled) && !override) {
|
||||
return
|
||||
}
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, !enabled)
|
||||
val controller = WindowInsetsControllerCompat(window, window.decorView)
|
||||
if (enabled) {
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
} else {
|
||||
val fullScreenThemeValue = TypedValue()
|
||||
val hasStatusBar = if (activity.theme.resolveAttribute(android.R.attr.windowFullscreen, fullScreenThemeValue, true) && fullScreenThemeValue.type == TypedValue.TYPE_INT_BOOLEAN) {
|
||||
fullScreenThemeValue.data == 0
|
||||
} else {
|
||||
// Fallback to checking the editor build
|
||||
!isEditorBuild()
|
||||
}
|
||||
|
||||
val types = if (hasStatusBar) {
|
||||
WindowInsetsCompat.Type.navigationBars() or WindowInsetsCompat.Type.statusBars()
|
||||
} else {
|
||||
WindowInsetsCompat.Type.navigationBars()
|
||||
}
|
||||
controller.show(types)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked from the render thread to toggle the immersive mode.
|
||||
*/
|
||||
@Keep
|
||||
private fun nativeEnableImmersiveMode(enabled: Boolean) {
|
||||
runOnUiThread {
|
||||
enableImmersiveMode(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun isInImmersiveMode() = useImmersive.get()
|
||||
|
||||
/**
|
||||
* Initializes the native layer of the Godot engine.
|
||||
*
|
||||
|
|
@ -413,13 +480,18 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
// ...add to FrameLayout
|
||||
containerLayout?.addView(editText)
|
||||
renderView = if (usesVulkan()) {
|
||||
if (!meetsVulkanRequirements(activity.packageManager)) {
|
||||
if (meetsVulkanRequirements(activity.packageManager)) {
|
||||
GodotVulkanRenderView(host, this, godotInputHandler)
|
||||
} else if (canFallbackToOpenGL()) {
|
||||
// Fallback to OpenGl.
|
||||
GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl)
|
||||
} else {
|
||||
throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message))
|
||||
}
|
||||
GodotVulkanRenderView(host, this)
|
||||
|
||||
} else {
|
||||
// Fallback to openGl
|
||||
GodotGLRenderView(host, this, xrMode, useDebugOpengl)
|
||||
// Fallback to OpenGl.
|
||||
GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl)
|
||||
}
|
||||
|
||||
if (host == primaryHost) {
|
||||
|
|
@ -520,45 +592,47 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
|
||||
fun onResume(host: GodotHost) {
|
||||
Log.v(TAG, "OnResume: $host")
|
||||
resumed = true
|
||||
if (host != primaryHost) {
|
||||
return
|
||||
}
|
||||
|
||||
renderView?.onActivityResumed()
|
||||
if (mAccelerometer != null) {
|
||||
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
|
||||
}
|
||||
if (mGravity != null) {
|
||||
mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME)
|
||||
}
|
||||
if (mMagnetometer != null) {
|
||||
mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME)
|
||||
}
|
||||
if (mGyroscope != null) {
|
||||
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
|
||||
}
|
||||
registerSensorsIfNeeded()
|
||||
enableImmersiveMode(useImmersive.get(), true)
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
plugin.onMainResume()
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerSensorsIfNeeded() {
|
||||
if (!resumed || !godotMainLoopStarted.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (accelerometerEnabled.get() && mAccelerometer != null) {
|
||||
mSensorManager.registerListener(godotInputHandler, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
|
||||
}
|
||||
if (gravityEnabled.get() && mGravity != null) {
|
||||
mSensorManager.registerListener(godotInputHandler, mGravity, SensorManager.SENSOR_DELAY_GAME)
|
||||
}
|
||||
if (magnetometerEnabled.get() && mMagnetometer != null) {
|
||||
mSensorManager.registerListener(godotInputHandler, mMagnetometer, SensorManager.SENSOR_DELAY_GAME)
|
||||
}
|
||||
if (gyroscopeEnabled.get() && mGyroscope != null) {
|
||||
mSensorManager.registerListener(godotInputHandler, mGyroscope, SensorManager.SENSOR_DELAY_GAME)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPause(host: GodotHost) {
|
||||
Log.v(TAG, "OnPause: $host")
|
||||
resumed = false
|
||||
if (host != primaryHost) {
|
||||
return
|
||||
}
|
||||
|
||||
renderView?.onActivityPaused()
|
||||
mSensorManager.unregisterListener(this)
|
||||
mSensorManager.unregisterListener(godotInputHandler)
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
plugin.onMainPause()
|
||||
}
|
||||
|
|
@ -604,6 +678,9 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
for (plugin in pluginRegistry.allPlugins) {
|
||||
plugin.onMainActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
FilePicker.handleActivityResult(context, requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -659,6 +736,17 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
*/
|
||||
private fun onGodotMainLoopStarted() {
|
||||
Log.v(TAG, "OnGodotMainLoopStarted")
|
||||
godotMainLoopStarted.set(true)
|
||||
|
||||
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")))
|
||||
gyroscopeEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gyroscope")))
|
||||
magnetometerEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_magnetometer")))
|
||||
|
||||
runOnUiThread {
|
||||
registerSensorsIfNeeded()
|
||||
enableImmersiveMode(useImmersive.get(), true)
|
||||
}
|
||||
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
plugin.onGodotMainLoopStarted()
|
||||
|
|
@ -679,11 +767,6 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
primaryHost?.onGodotRestartRequested(this)
|
||||
}
|
||||
|
||||
private fun registerUiChangeListener() {
|
||||
val decorView = requireActivity().window.decorView
|
||||
decorView.setOnSystemUiVisibilityChangeListener(uiChangeListener)
|
||||
}
|
||||
|
||||
fun alert(
|
||||
@StringRes messageResId: Int,
|
||||
@StringRes titleResId: Int,
|
||||
|
|
@ -701,7 +784,7 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(message).setTitle(title)
|
||||
builder.setPositiveButton(
|
||||
"OK"
|
||||
R.string.dialog_ok
|
||||
) { dialog: DialogInterface, id: Int ->
|
||||
okCallback?.run()
|
||||
dialog.cancel()
|
||||
|
|
@ -740,9 +823,33 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
* 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
|
||||
var rendererSource = "ProjectSettings"
|
||||
var renderer = GodotLib.getGlobal("rendering/renderer/rendering_method")
|
||||
var renderingDeviceSource = "ProjectSettings"
|
||||
var renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver")
|
||||
val cmdline = getCommandLine()
|
||||
var index = cmdline.indexOf("--rendering-method")
|
||||
if (index > -1 && cmdline.size > index + 1) {
|
||||
rendererSource = "CommandLine"
|
||||
renderer = cmdline.get(index + 1)
|
||||
}
|
||||
index = cmdline.indexOf("--rendering-driver")
|
||||
if (index > -1 && cmdline.size > index + 1) {
|
||||
renderingDeviceSource = "CommandLine"
|
||||
renderingDevice = cmdline.get(index + 1)
|
||||
}
|
||||
val result = ("forward_plus" == renderer || "mobile" == renderer) && "vulkan" == renderingDevice
|
||||
Log.d(TAG, """usesVulkan(): ${result}
|
||||
renderingDevice: ${renderingDevice} (${renderingDeviceSource})
|
||||
renderer: ${renderer} (${rendererSource})""")
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if can fallback to OpenGL.
|
||||
*/
|
||||
private fun canFallbackToOpenGL(): Boolean {
|
||||
return java.lang.Boolean.parseBoolean(GodotLib.getGlobal("rendering/rendering_device/fallback_to_opengl3"))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -794,11 +901,6 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
return mClipboard.hasPrimaryClip()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this is an editor build, false if this is a template build
|
||||
*/
|
||||
fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
|
||||
|
||||
fun getClipboard(): String {
|
||||
val clipData = mClipboard.primaryClip ?: return ""
|
||||
val text = clipData.getItemAt(0).text ?: return ""
|
||||
|
|
@ -810,6 +912,51 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
mClipboard.setPrimaryClip(clip)
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun showFilePicker(currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
FilePicker.showFilePicker(context, getActivity(), currentDirectory, filename, fileMode, filters)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method shows a dialog with multiple buttons.
|
||||
*
|
||||
* @param title The title of the dialog.
|
||||
* @param message The message displayed in the dialog.
|
||||
* @param buttons An array of button labels to display.
|
||||
*/
|
||||
@Keep
|
||||
private fun showDialog(title: String, message: String, buttons: Array<String>) {
|
||||
getActivity()?.let { DialogUtils.showDialog(it, title, message, buttons) }
|
||||
}
|
||||
|
||||
/**
|
||||
* This method shows a dialog with a text input field, allowing the user to input text.
|
||||
*
|
||||
* @param title The title of the input dialog.
|
||||
* @param message The message displayed in the input dialog.
|
||||
* @param existingText The existing text that will be pre-filled in the input field.
|
||||
*/
|
||||
@Keep
|
||||
private fun showInputDialog(title: String, message: String, existingText: String) {
|
||||
getActivity()?.let { DialogUtils.showInputDialog(it, title, message, existingText) }
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun getAccentColor(): Int {
|
||||
val value = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.colorAccent, value, true)
|
||||
return value.data
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun getBaseColor(): Int {
|
||||
val value = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.colorBackground, value, true)
|
||||
return value.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the Godot Engine and kill the process it's running in.
|
||||
*/
|
||||
|
|
@ -847,88 +994,12 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
}
|
||||
|
||||
fun onBackPressed() {
|
||||
var shouldQuit = true
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
if (plugin.onMainBackPressed()) {
|
||||
shouldQuit = false
|
||||
}
|
||||
}
|
||||
if (shouldQuit) {
|
||||
renderView?.queueOnRenderThread { GodotLib.back() }
|
||||
plugin.onMainBackPressed()
|
||||
}
|
||||
renderView?.queueOnRenderThread { GodotLib.back() }
|
||||
}
|
||||
|
||||
private fun getRotatedValues(values: FloatArray?): FloatArray? {
|
||||
if (values == null || values.size != 3) {
|
||||
return null
|
||||
}
|
||||
val rotatedValues = FloatArray(3)
|
||||
when (windowManager.defaultDisplay.rotation) {
|
||||
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
|
||||
}
|
||||
|
||||
val rotatedValues = getRotatedValues(event.values)
|
||||
|
||||
when (event.sensor.type) {
|
||||
Sensor.TYPE_ACCELEROMETER -> {
|
||||
rotatedValues?.let {
|
||||
renderView?.queueOnRenderThread {
|
||||
GodotLib.accelerometer(-it[0], -it[1], -it[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
Sensor.TYPE_GRAVITY -> {
|
||||
rotatedValues?.let {
|
||||
renderView?.queueOnRenderThread {
|
||||
GodotLib.gravity(-it[0], -it[1], -it[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
Sensor.TYPE_MAGNETIC_FIELD -> {
|
||||
rotatedValues?.let {
|
||||
renderView?.queueOnRenderThread {
|
||||
GodotLib.magnetometer(-it[0], -it[1], -it[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
Sensor.TYPE_GYROSCOPE -> {
|
||||
rotatedValues?.let {
|
||||
renderView?.queueOnRenderThread {
|
||||
GodotLib.gyroscope(it[0], it[1], it[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
||||
|
||||
/**
|
||||
* Used by the native code (java_godot_wrapper.h) to vibrate the device.
|
||||
* @param durationMs
|
||||
|
|
@ -985,7 +1056,8 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
}
|
||||
|
||||
fun requestPermission(name: String?): Boolean {
|
||||
return requestPermission(name, getActivity())
|
||||
val activity = getActivity() ?: return false
|
||||
return requestPermission(name, activity)
|
||||
}
|
||||
|
||||
fun requestPermissions(): Boolean {
|
||||
|
|
@ -996,11 +1068,25 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
return PermissionsUtil.getGrantedPermissions(getActivity())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is the Godot editor.
|
||||
*/
|
||||
fun isEditorHint() = isEditorBuild() && GodotLib.isEditorHint()
|
||||
|
||||
/**
|
||||
* Returns true if this is the Godot project manager.
|
||||
*/
|
||||
fun isProjectManagerHint() = isEditorBuild() && GodotLib.isProjectManagerHint()
|
||||
|
||||
/**
|
||||
* Return true if the given feature is supported.
|
||||
*/
|
||||
@Keep
|
||||
private fun hasFeature(feature: String): Boolean {
|
||||
if (primaryHost?.supportsFeature(feature) ?: false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
if (plugin.supportsFeature(feature)) {
|
||||
return true
|
||||
|
|
@ -1063,12 +1149,12 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
|
||||
@Keep
|
||||
private fun initInputDevices() {
|
||||
renderView?.initInputDevices()
|
||||
godotInputHandler.initInputDevices()
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun createNewGodotInstance(args: Array<String>): Int {
|
||||
return primaryHost?.onNewGodotInstanceRequested(args) ?: 0
|
||||
return primaryHost?.onNewGodotInstanceRequested(args) ?: -1
|
||||
}
|
||||
|
||||
@Keep
|
||||
|
|
@ -1085,4 +1171,25 @@ class Godot(private val context: Context) : SensorEventListener {
|
|||
private fun nativeDumpBenchmark(benchmarkFile: String) {
|
||||
dumpBenchmark(fileAccessHandler, benchmarkFile)
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeSignApk(inputPath: String,
|
||||
outputPath: String,
|
||||
keystorePath: String,
|
||||
keystoreUser: String,
|
||||
keystorePassword: String): Int {
|
||||
val signResult = primaryHost?.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword) ?: Error.ERR_UNAVAILABLE
|
||||
return signResult.toNativeValue()
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeVerifyApk(apkPath: String): Int {
|
||||
val verifyResult = primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE
|
||||
return verifyResult.toNativeValue()
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeOnEditorWorkspaceSelected(workspace: String) {
|
||||
primaryHost?.onEditorWorkspaceSelected(workspace)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
package org.godotengine.godot
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
|
|
@ -53,19 +54,31 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||
private val TAG = GodotActivity::class.java.simpleName
|
||||
|
||||
@JvmStatic
|
||||
protected val EXTRA_FORCE_QUIT = "force_quit_requested"
|
||||
protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
|
||||
|
||||
@JvmStatic
|
||||
protected val EXTRA_NEW_LAUNCH = "new_launch_requested"
|
||||
|
||||
// This window must not match those in BaseGodotEditor.RUN_GAME_INFO etc
|
||||
@JvmStatic
|
||||
private final val DEFAULT_WINDOW_ID = 664;
|
||||
}
|
||||
|
||||
private val commandLineParams = ArrayList<String>()
|
||||
/**
|
||||
* Interaction with the [Godot] object is delegated to the [GodotFragment] class.
|
||||
*/
|
||||
protected var godotFragment: GodotFragment? = null
|
||||
private set
|
||||
|
||||
@CallSuper
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}")
|
||||
commandLineParams.addAll(params ?: emptyArray())
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(getGodotAppLayout())
|
||||
|
||||
handleStartIntent(intent, true)
|
||||
|
|
@ -81,6 +94,29 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onNewGodotInstanceRequested(args: Array<String>): Int {
|
||||
Log.d(TAG, "Restarting with parameters ${args.contentToString()}")
|
||||
val intent = Intent()
|
||||
.setComponent(ComponentName(this, javaClass.name))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(EXTRA_COMMAND_LINE_PARAMS, args)
|
||||
triggerRebirth(null, intent)
|
||||
// fake 'process' id returned by create_instance() etc
|
||||
return DEFAULT_WINDOW_ID;
|
||||
}
|
||||
|
||||
protected fun triggerRebirth(bundle: Bundle?, intent: Intent) {
|
||||
// Launch a new activity
|
||||
val godot = godot
|
||||
if (godot != null) {
|
||||
godot.destroyAndKillProcess {
|
||||
ProcessPhoenix.triggerRebirth(this, bundle, intent)
|
||||
}
|
||||
} else {
|
||||
ProcessPhoenix.triggerRebirth(this, bundle, intent)
|
||||
}
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
protected open fun getGodotAppLayout() = R.layout.godot_app_layout
|
||||
|
||||
|
|
@ -128,12 +164,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -184,4 +214,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
|||
protected open fun initGodotInstance(): GodotFragment {
|
||||
return GodotFragment()
|
||||
}
|
||||
|
||||
override fun getCommandLine(): MutableList<String> = commandLineParams
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.error.Error;
|
||||
import org.godotengine.godot.plugin.GodotPlugin;
|
||||
import org.godotengine.godot.utils.BenchmarkUtils;
|
||||
|
||||
|
|
@ -473,7 +474,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
|||
if (parentHost != null) {
|
||||
return parentHost.onNewGodotInstanceRequested(args);
|
||||
}
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -484,4 +485,35 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
|||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) {
|
||||
if (parentHost != null) {
|
||||
return parentHost.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword);
|
||||
}
|
||||
return Error.ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Error verifyApk(@NonNull String apkPath) {
|
||||
if (parentHost != null) {
|
||||
return parentHost.verifyApk(apkPath);
|
||||
}
|
||||
return Error.ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFeature(String featureTag) {
|
||||
if (parentHost != null) {
|
||||
return parentHost.supportsFeature(featureTag);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEditorWorkspaceSelected(String workspace) {
|
||||
if (parentHost != null) {
|
||||
parentHost.onEditorWorkspaceSelected(workspace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,12 +83,12 @@ class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
|
|||
private final GodotRenderer godotRenderer;
|
||||
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
|
||||
|
||||
public GodotGLRenderView(GodotHost host, Godot godot, XRMode xrMode, boolean useDebugOpengl) {
|
||||
public GodotGLRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl) {
|
||||
super(host.getActivity());
|
||||
|
||||
this.host = host;
|
||||
this.godot = godot;
|
||||
this.inputHandler = new GodotInputHandler(this);
|
||||
this.inputHandler = inputHandler;
|
||||
this.godotRenderer = new GodotRenderer();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
|
||||
|
|
@ -101,11 +101,6 @@ class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initInputDevices() {
|
||||
this.inputHandler.initInputDevices();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueOnRenderThread(Runnable event) {
|
||||
queueEvent(event);
|
||||
|
|
@ -144,11 +139,6 @@ class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
|
|||
requestRenderThreadExitAndWait();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
godot.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GodotInputHandler getInputHandler() {
|
||||
return inputHandler;
|
||||
|
|
|
|||
|
|
@ -30,10 +30,13 @@
|
|||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.error.Error;
|
||||
import org.godotengine.godot.plugin.GodotPlugin;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
|
@ -89,7 +92,7 @@ public interface GodotHost {
|
|||
* @return the id of the new instance. See {@code onGodotForceQuit}
|
||||
*/
|
||||
default int onNewGodotInstanceRequested(String[] args) {
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -108,4 +111,43 @@ public interface GodotHost {
|
|||
default Set<GodotPlugin> getHostPlugins(Godot engine) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the given Android apk
|
||||
*
|
||||
* @param inputPath Path to the apk that should be signed
|
||||
* @param outputPath Path for the signed output apk; can be the same as inputPath
|
||||
* @param keystorePath Path to the keystore to use for signing the apk
|
||||
* @param keystoreUser Keystore user credential
|
||||
* @param keystorePassword Keystore password credential
|
||||
*
|
||||
* @return {@link Error#OK} if signing is successful
|
||||
*/
|
||||
default Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) {
|
||||
return Error.ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the given Android apk is signed
|
||||
*
|
||||
* @param apkPath Path to the apk that should be verified
|
||||
* @return {@link Error#OK} if verification was successful
|
||||
*/
|
||||
default Error verifyApk(@NonNull String apkPath) {
|
||||
return Error.ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given feature tag is supported.
|
||||
*
|
||||
* @see <a href="https://docs.godotengine.org/en/stable/tutorials/export/feature_tags.html">Feature tags</a>
|
||||
*/
|
||||
default boolean supportsFeature(String featureTag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when an editor workspace has been selected.
|
||||
*/
|
||||
default void onEditorWorkspaceSelected(String workspace) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,13 +30,12 @@
|
|||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.error.Error;
|
||||
import org.godotengine.godot.input.GodotEditText;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
|
@ -120,10 +119,10 @@ public class GodotIO {
|
|||
}
|
||||
|
||||
activity.startActivity(intent);
|
||||
return 0;
|
||||
return Error.OK.toNativeValue();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Unable to open uri " + uriString, e);
|
||||
return 1;
|
||||
return Error.FAILED.toNativeValue();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +130,18 @@ public class GodotIO {
|
|||
return activity.getCacheDir().getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getTempDir() {
|
||||
File tempDir = new File(getCacheDir() + "/tmp");
|
||||
|
||||
if (!tempDir.exists()) {
|
||||
if (!tempDir.mkdirs()) {
|
||||
Log.e(TAG, "Unable to create temp dir");
|
||||
}
|
||||
}
|
||||
|
||||
return tempDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getDataDir() {
|
||||
return activity.getFilesDir().getAbsolutePath();
|
||||
}
|
||||
|
|
@ -216,6 +227,14 @@ public class GodotIO {
|
|||
return result;
|
||||
}
|
||||
|
||||
public boolean hasHardwareKeyboard() {
|
||||
if (edit != null) {
|
||||
return edit.hasHardwareKeyboard();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void showKeyboard(String p_existing_text, int p_type, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
|
||||
if (edit != null) {
|
||||
edit.showKeyboard(p_existing_text, GodotEditText.VirtualKeyboardType.values()[p_type], p_max_input_length, p_cursor_start, p_cursor_end);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import org.godotengine.godot.io.directory.DirectoryAccessHandler;
|
|||
import org.godotengine.godot.io.file.FileAccessHandler;
|
||||
import org.godotengine.godot.tts.GodotTTS;
|
||||
import org.godotengine.godot.utils.GodotNetUtils;
|
||||
import org.godotengine.godot.variant.Callable;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.AssetManager;
|
||||
|
|
@ -195,21 +196,55 @@ public class GodotLib {
|
|||
*/
|
||||
public static native String getEditorSetting(String settingKey);
|
||||
|
||||
/**
|
||||
* Update the 'key' editor setting with the given data. Must be called on the render thread.
|
||||
* @param key
|
||||
* @param data
|
||||
*/
|
||||
public static native void setEditorSetting(String key, Object data);
|
||||
|
||||
/**
|
||||
* Used to access project metadata from the editor settings. Must be accessed on the render thread.
|
||||
* @param section
|
||||
* @param key
|
||||
* @param defaultValue
|
||||
* @return
|
||||
*/
|
||||
public static native Object getEditorProjectMetadata(String section, String key, Object defaultValue);
|
||||
|
||||
/**
|
||||
* Set the project metadata to the editor settings. Must be accessed on the render thread.
|
||||
* @param section
|
||||
* @param key
|
||||
* @param data
|
||||
*/
|
||||
public static native void setEditorProjectMetadata(String section, String key, Object data);
|
||||
|
||||
/**
|
||||
* Invoke method |p_method| on the Godot object specified by |p_id|
|
||||
* @param p_id Id of the Godot object to invoke
|
||||
* @param p_method Name of the method to invoke
|
||||
* @param p_params Parameters to use for method invocation
|
||||
*
|
||||
* @deprecated Use {@link Callable#call(long, String, Object...)} instead.
|
||||
*/
|
||||
public static native void callobject(long p_id, String p_method, Object[] p_params);
|
||||
@Deprecated
|
||||
public static void callobject(long p_id, String p_method, Object[] p_params) {
|
||||
Callable.call(p_id, p_method, p_params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke method |p_method| on the Godot object specified by |p_id| during idle time.
|
||||
* @param p_id Id of the Godot object to invoke
|
||||
* @param p_method Name of the method to invoke
|
||||
* @param p_params Parameters to use for method invocation
|
||||
*
|
||||
* @deprecated Use {@link Callable#callDeferred(long, String, Object...)} instead.
|
||||
*/
|
||||
public static native void calldeferred(long p_id, String p_method, Object[] p_params);
|
||||
@Deprecated
|
||||
public static void calldeferred(long p_id, String p_method, Object[] p_params) {
|
||||
Callable.callDeferred(p_id, p_method, p_params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward the results from a permission request.
|
||||
|
|
@ -224,6 +259,11 @@ public class GodotLib {
|
|||
*/
|
||||
public static native void onNightModeChanged();
|
||||
|
||||
/**
|
||||
* Invoked on the file picker closed.
|
||||
*/
|
||||
public static native void filePickerCallback(boolean p_ok, String[] p_selected_paths);
|
||||
|
||||
/**
|
||||
* Invoked on the GL thread to configure the height of the virtual keyboard.
|
||||
*/
|
||||
|
|
@ -246,4 +286,13 @@ public class GodotLib {
|
|||
* dispatched from the UI thread.
|
||||
*/
|
||||
public static native boolean shouldDispatchInputToRenderThread();
|
||||
|
||||
/**
|
||||
* @return the project resource directory
|
||||
*/
|
||||
public static native String getProjectResourceDir();
|
||||
|
||||
static native boolean isEditorHint();
|
||||
|
||||
static native boolean isProjectManagerHint();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,14 +31,13 @@
|
|||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.input.GodotInputHandler;
|
||||
import org.godotengine.godot.utils.DeviceUtils;
|
||||
|
||||
import android.view.SurfaceView;
|
||||
|
||||
public interface GodotRenderView {
|
||||
SurfaceView getView();
|
||||
|
||||
void initInputDevices();
|
||||
|
||||
/**
|
||||
* Starts the thread that will drive Godot's rendering.
|
||||
*/
|
||||
|
|
@ -59,15 +58,17 @@ public interface GodotRenderView {
|
|||
|
||||
void onActivityDestroyed();
|
||||
|
||||
void onBackPressed();
|
||||
|
||||
GodotInputHandler getInputHandler();
|
||||
|
||||
void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY);
|
||||
|
||||
void setPointerIcon(int pointerType);
|
||||
|
||||
/**
|
||||
* @return true if pointer capture is supported.
|
||||
*/
|
||||
default boolean canCapturePointer() {
|
||||
return getInputHandler().canCapturePointer();
|
||||
// Pointer capture is not supported on native XR devices.
|
||||
return !DeviceUtils.isNativeXRDevice(getView().getContext()) && getInputHandler().canCapturePointer();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,17 +57,18 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
|
|||
private final VkRenderer mRenderer;
|
||||
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
|
||||
|
||||
public GodotVulkanRenderView(GodotHost host, Godot godot) {
|
||||
public GodotVulkanRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler) {
|
||||
super(host.getActivity());
|
||||
|
||||
this.host = host;
|
||||
this.godot = godot;
|
||||
mInputHandler = new GodotInputHandler(this);
|
||||
mInputHandler = inputHandler;
|
||||
mRenderer = new VkRenderer();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
|
||||
}
|
||||
setFocusableInTouchMode(true);
|
||||
setClickable(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -80,11 +81,6 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initInputDevices() {
|
||||
mInputHandler.initInputDevices();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueOnRenderThread(Runnable event) {
|
||||
queueOnVkThread(event);
|
||||
|
|
@ -123,11 +119,6 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
|
|||
requestRenderThreadExitAndWait();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
godot.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GodotInputHandler getInputHandler() {
|
||||
return mInputHandler;
|
||||
|
|
@ -142,17 +133,17 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
|
|||
|
||||
@Override
|
||||
public boolean onKeyUp(final int keyCode, KeyEvent event) {
|
||||
return mInputHandler.onKeyUp(keyCode, event);
|
||||
return mInputHandler.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(final int keyCode, KeyEvent event) {
|
||||
return mInputHandler.onKeyDown(keyCode, event);
|
||||
return mInputHandler.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
return mInputHandler.onGenericMotionEvent(event);
|
||||
return mInputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
/**************************************************************************/
|
||||
/* Error.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.error
|
||||
|
||||
/**
|
||||
* Godot error list.
|
||||
*
|
||||
* This enum MUST match its native counterpart in 'core/error/error_list.h'
|
||||
*/
|
||||
enum class Error(private val description: String) {
|
||||
OK("OK"), // (0)
|
||||
FAILED("Failed"), ///< Generic fail error
|
||||
ERR_UNAVAILABLE("Unavailable"), ///< What is requested is unsupported/unavailable
|
||||
ERR_UNCONFIGURED("Unconfigured"), ///< The object being used hasn't been properly set up yet
|
||||
ERR_UNAUTHORIZED("Unauthorized"), ///< Missing credentials for requested resource
|
||||
ERR_PARAMETER_RANGE_ERROR("Parameter out of range"), ///< Parameter given out of range (5)
|
||||
ERR_OUT_OF_MEMORY("Out of memory"), ///< Out of memory
|
||||
ERR_FILE_NOT_FOUND("File not found"),
|
||||
ERR_FILE_BAD_DRIVE("File: Bad drive"),
|
||||
ERR_FILE_BAD_PATH("File: Bad path"),
|
||||
ERR_FILE_NO_PERMISSION("File: Permission denied"), // (10)
|
||||
ERR_FILE_ALREADY_IN_USE("File already in use"),
|
||||
ERR_FILE_CANT_OPEN("Can't open file"),
|
||||
ERR_FILE_CANT_WRITE("Can't write file"),
|
||||
ERR_FILE_CANT_READ("Can't read file"),
|
||||
ERR_FILE_UNRECOGNIZED("File unrecognized"), // (15)
|
||||
ERR_FILE_CORRUPT("File corrupt"),
|
||||
ERR_FILE_MISSING_DEPENDENCIES("Missing dependencies for file"),
|
||||
ERR_FILE_EOF("End of file"),
|
||||
ERR_CANT_OPEN("Can't open"), ///< Can't open a resource/socket/file
|
||||
ERR_CANT_CREATE("Can't create"), // (20)
|
||||
ERR_QUERY_FAILED("Query failed"),
|
||||
ERR_ALREADY_IN_USE("Already in use"),
|
||||
ERR_LOCKED("Locked"), ///< resource is locked
|
||||
ERR_TIMEOUT("Timeout"),
|
||||
ERR_CANT_CONNECT("Can't connect"), // (25)
|
||||
ERR_CANT_RESOLVE("Can't resolve"),
|
||||
ERR_CONNECTION_ERROR("Connection error"),
|
||||
ERR_CANT_ACQUIRE_RESOURCE("Can't acquire resource"),
|
||||
ERR_CANT_FORK("Can't fork"),
|
||||
ERR_INVALID_DATA("Invalid data"), ///< Data passed is invalid (30)
|
||||
ERR_INVALID_PARAMETER("Invalid parameter"), ///< Parameter passed is invalid
|
||||
ERR_ALREADY_EXISTS("Already exists"), ///< When adding, item already exists
|
||||
ERR_DOES_NOT_EXIST("Does not exist"), ///< When retrieving/erasing, if item does not exist
|
||||
ERR_DATABASE_CANT_READ("Can't read database"), ///< database is full
|
||||
ERR_DATABASE_CANT_WRITE("Can't write database"), ///< database is full (35)
|
||||
ERR_COMPILATION_FAILED("Compilation failed"),
|
||||
ERR_METHOD_NOT_FOUND("Method not found"),
|
||||
ERR_LINK_FAILED("Link failed"),
|
||||
ERR_SCRIPT_FAILED("Script failed"),
|
||||
ERR_CYCLIC_LINK("Cyclic link detected"), // (40)
|
||||
ERR_INVALID_DECLARATION("Invalid declaration"),
|
||||
ERR_DUPLICATE_SYMBOL("Duplicate symbol"),
|
||||
ERR_PARSE_ERROR("Parse error"),
|
||||
ERR_BUSY("Busy"),
|
||||
ERR_SKIP("Skip"), // (45)
|
||||
ERR_HELP("Help"), ///< user requested help!!
|
||||
ERR_BUG("Bug"), ///< a bug in the software certainly happened, due to a double check failing or unexpected behavior.
|
||||
ERR_PRINTER_ON_FIRE("Printer on fire"); /// the parallel port printer is engulfed in flames
|
||||
|
||||
companion object {
|
||||
internal fun fromNativeValue(nativeValue: Int): Error? {
|
||||
return Error.entries.getOrNull(nativeValue)
|
||||
}
|
||||
}
|
||||
|
||||
fun toNativeValue(): Int = this.ordinal
|
||||
|
||||
override fun toString(): String {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
|
@ -264,7 +264,7 @@ public class GodotEditText extends EditText {
|
|||
isModifiedKey;
|
||||
}
|
||||
|
||||
boolean hasHardwareKeyboard() {
|
||||
public boolean hasHardwareKeyboard() {
|
||||
Configuration config = getResources().getConfiguration();
|
||||
boolean hasHardwareKeyboardConfig = config.keyboard != Configuration.KEYBOARD_NOKEYS &&
|
||||
config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO;
|
||||
|
|
|
|||
|
|
@ -32,10 +32,14 @@ package org.godotengine.godot.input;
|
|||
|
||||
import static org.godotengine.godot.utils.GLUtils.DEBUG;
|
||||
|
||||
import org.godotengine.godot.Godot;
|
||||
import org.godotengine.godot.GodotLib;
|
||||
import org.godotengine.godot.GodotRenderView;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
|
@ -46,6 +50,10 @@ import android.view.InputDevice;
|
|||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.Surface;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
|
@ -54,7 +62,7 @@ import java.util.Set;
|
|||
/**
|
||||
* Handles input related events for the {@link GodotRenderView} view.
|
||||
*/
|
||||
public class GodotInputHandler implements InputManager.InputDeviceListener {
|
||||
public class GodotInputHandler implements InputManager.InputDeviceListener, SensorEventListener {
|
||||
private static final String TAG = GodotInputHandler.class.getSimpleName();
|
||||
|
||||
private static final int ROTARY_INPUT_VERTICAL_AXIS = 1;
|
||||
|
|
@ -64,8 +72,9 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4);
|
||||
private final HashSet<Integer> mHardwareKeyboardIds = new HashSet<>();
|
||||
|
||||
private final GodotRenderView mRenderView;
|
||||
private final Godot godot;
|
||||
private final InputManager mInputManager;
|
||||
private final WindowManager windowManager;
|
||||
private final GestureDetector gestureDetector;
|
||||
private final ScaleGestureDetector scaleGestureDetector;
|
||||
private final GodotGestureHandler godotGestureHandler;
|
||||
|
|
@ -77,12 +86,13 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
|
||||
private int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS;
|
||||
|
||||
public GodotInputHandler(GodotRenderView godotView) {
|
||||
final Context context = godotView.getView().getContext();
|
||||
mRenderView = godotView;
|
||||
public GodotInputHandler(Context context, Godot godot) {
|
||||
this.godot = godot;
|
||||
mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
|
||||
mInputManager.registerInputDeviceListener(this, null);
|
||||
|
||||
windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
|
||||
|
||||
this.godotGestureHandler = new GodotGestureHandler(this);
|
||||
this.gestureDetector = new GestureDetector(context, godotGestureHandler);
|
||||
this.gestureDetector.setIsLongpressEnabled(false);
|
||||
|
|
@ -144,10 +154,6 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
}
|
||||
|
||||
public boolean onKeyUp(final int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -173,13 +179,6 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
}
|
||||
|
||||
public boolean onKeyDown(final int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
mRenderView.onBackPressed();
|
||||
// press 'back' button should not terminate program
|
||||
//normal handle 'back' event in game logic
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -507,7 +506,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
return handleTouchEvent(event, eventActionOverride, doubleTap);
|
||||
}
|
||||
|
||||
private static float getEventTiltX(MotionEvent event) {
|
||||
static float getEventTiltX(MotionEvent event) {
|
||||
// Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise.
|
||||
final float orientation = event.getOrientation();
|
||||
|
||||
|
|
@ -520,7 +519,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
return (float)-Math.sin(orientation) * tiltMult;
|
||||
}
|
||||
|
||||
private static float getEventTiltY(MotionEvent event) {
|
||||
static float getEventTiltY(MotionEvent event) {
|
||||
// Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise.
|
||||
final float orientation = event.getOrientation();
|
||||
|
||||
|
|
@ -579,6 +578,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
}
|
||||
|
||||
boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) {
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fix the buttonsMask
|
||||
switch (eventAction) {
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
|
|
@ -594,7 +598,6 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
break;
|
||||
}
|
||||
|
||||
final int updatedButtonsMask = buttonsMask;
|
||||
// We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically
|
||||
// follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate
|
||||
// stream of events to the engine.
|
||||
|
|
@ -607,11 +610,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
case MotionEvent.ACTION_SCROLL: {
|
||||
if (shouldDispatchInputToRenderThread()) {
|
||||
mRenderView.queueOnRenderThread(() -> GodotLib.dispatchMouseEvent(eventAction, updatedButtonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY));
|
||||
} else {
|
||||
GodotLib.dispatchMouseEvent(eventAction, updatedButtonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY);
|
||||
}
|
||||
runnable.setMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -627,22 +627,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
}
|
||||
|
||||
boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
|
||||
final int pointerCount = event.getPointerCount();
|
||||
if (pointerCount == 0) {
|
||||
if (event.getPointerCount() == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final float[] positions = new float[pointerCount * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc...
|
||||
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
positions[i * 6 + 0] = event.getPointerId(i);
|
||||
positions[i * 6 + 1] = event.getX(i);
|
||||
positions[i * 6 + 2] = event.getY(i);
|
||||
positions[i * 6 + 3] = event.getPressure(i);
|
||||
positions[i * 6 + 4] = getEventTiltX(event);
|
||||
positions[i * 6 + 5] = getEventTiltY(event);
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return false;
|
||||
}
|
||||
final int actionPointerId = event.getPointerId(event.getActionIndex());
|
||||
|
||||
switch (eventActionOverride) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
|
|
@ -651,11 +643,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
case MotionEvent.ACTION_MOVE:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||
if (shouldDispatchInputToRenderThread()) {
|
||||
mRenderView.queueOnRenderThread(() -> GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap));
|
||||
} else {
|
||||
GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap);
|
||||
}
|
||||
runnable.setTouchEvent(event, eventActionOverride, doubleTap);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -663,58 +652,128 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
|
|||
}
|
||||
|
||||
void handleMagnifyEvent(float x, float y, float factor) {
|
||||
if (shouldDispatchInputToRenderThread()) {
|
||||
mRenderView.queueOnRenderThread(() -> GodotLib.magnify(x, y, factor));
|
||||
} else {
|
||||
GodotLib.magnify(x, y, factor);
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setMagnifyEvent(x, y, factor);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
void handlePanEvent(float x, float y, float deltaX, float deltaY) {
|
||||
if (shouldDispatchInputToRenderThread()) {
|
||||
mRenderView.queueOnRenderThread(() -> GodotLib.pan(x, y, deltaX, deltaY));
|
||||
} else {
|
||||
GodotLib.pan(x, y, deltaX, deltaY);
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setPanEvent(x, y, deltaX, deltaY);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
private void handleJoystickButtonEvent(int device, int button, boolean pressed) {
|
||||
if (shouldDispatchInputToRenderThread()) {
|
||||
mRenderView.queueOnRenderThread(() -> GodotLib.joybutton(device, button, pressed));
|
||||
} else {
|
||||
GodotLib.joybutton(device, button, pressed);
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setJoystickButtonEvent(device, button, pressed);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
private void handleJoystickAxisEvent(int device, int axis, float value) {
|
||||
if (shouldDispatchInputToRenderThread()) {
|
||||
mRenderView.queueOnRenderThread(() -> GodotLib.joyaxis(device, axis, value));
|
||||
} else {
|
||||
GodotLib.joyaxis(device, axis, value);
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setJoystickAxisEvent(device, axis, value);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
private void handleJoystickHatEvent(int device, int hatX, int hatY) {
|
||||
if (shouldDispatchInputToRenderThread()) {
|
||||
mRenderView.queueOnRenderThread(() -> GodotLib.joyhat(device, hatX, hatY));
|
||||
} else {
|
||||
GodotLib.joyhat(device, hatX, hatY);
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setJoystickHatEvent(device, hatX, hatY);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
private void handleJoystickConnectionChangedEvent(int device, boolean connected, String name) {
|
||||
if (shouldDispatchInputToRenderThread()) {
|
||||
mRenderView.queueOnRenderThread(() -> GodotLib.joyconnectionchanged(device, connected, name));
|
||||
} else {
|
||||
GodotLib.joyconnectionchanged(device, connected, name);
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setJoystickConnectionChangedEvent(device, connected, name);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
void handleKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) {
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setKeyEvent(physicalKeycode, unicode, keyLabel, pressed, echo);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
private void dispatchInputEventRunnable(@NonNull InputEventRunnable runnable) {
|
||||
if (shouldDispatchInputToRenderThread()) {
|
||||
mRenderView.queueOnRenderThread(() -> GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo));
|
||||
godot.runOnRenderThread(runnable);
|
||||
} else {
|
||||
GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo);
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
final float[] values = event.values;
|
||||
if (values == null || values.length != 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
float rotatedValue0 = 0f;
|
||||
float rotatedValue1 = 0f;
|
||||
float rotatedValue2 = 0f;
|
||||
switch (windowManager.getDefaultDisplay().getRotation()) {
|
||||
case Surface.ROTATION_0:
|
||||
rotatedValue0 = values[0];
|
||||
rotatedValue1 = values[1];
|
||||
rotatedValue2 = values[2];
|
||||
break;
|
||||
|
||||
case Surface.ROTATION_90:
|
||||
rotatedValue0 = -values[1];
|
||||
rotatedValue1 = values[0];
|
||||
rotatedValue2 = values[2];
|
||||
break;
|
||||
|
||||
case Surface.ROTATION_180:
|
||||
rotatedValue0 = -values[0];
|
||||
rotatedValue1 = -values[1];
|
||||
rotatedValue2 = values[2];
|
||||
break;
|
||||
|
||||
case Surface.ROTATION_270:
|
||||
rotatedValue0 = values[1];
|
||||
rotatedValue1 = -values[0];
|
||||
rotatedValue2 = values[2];
|
||||
break;
|
||||
}
|
||||
|
||||
runnable.setSensorEvent(event.sensor.getType(), rotatedValue0, rotatedValue1, rotatedValue2);
|
||||
godot.runOnRenderThread(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,353 @@
|
|||
/**************************************************************************/
|
||||
/* InputEventRunnable.java */
|
||||
/**************************************************************************/
|
||||
/* 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.input;
|
||||
|
||||
import org.godotengine.godot.GodotLib;
|
||||
|
||||
import android.hardware.Sensor;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pools;
|
||||
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Used to dispatch input events.
|
||||
*
|
||||
* This is a specialized version of @{@link Runnable} which allows to allocate a finite pool of
|
||||
* objects for input events dispatching, thus avoid the creation (and garbage collection) of
|
||||
* spurious @{@link Runnable} objects.
|
||||
*/
|
||||
final class InputEventRunnable implements Runnable {
|
||||
private static final String TAG = InputEventRunnable.class.getSimpleName();
|
||||
|
||||
private static final int MAX_TOUCH_POINTER_COUNT = 10; // assuming 10 fingers as max supported concurrent touch pointers
|
||||
|
||||
private static final Pools.Pool<InputEventRunnable> POOL = new Pools.Pool<>() {
|
||||
private static final int MAX_POOL_SIZE = 120 * 10; // up to 120Hz input events rate for up to 5 secs (ANR limit) * 2
|
||||
|
||||
private final ArrayBlockingQueue<InputEventRunnable> queue = new ArrayBlockingQueue<>(MAX_POOL_SIZE);
|
||||
private final AtomicInteger createdCount = new AtomicInteger();
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public InputEventRunnable acquire() {
|
||||
InputEventRunnable instance = queue.poll();
|
||||
if (instance == null) {
|
||||
int creationCount = createdCount.incrementAndGet();
|
||||
if (creationCount <= MAX_POOL_SIZE) {
|
||||
instance = new InputEventRunnable(creationCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean release(@NonNull InputEventRunnable instance) {
|
||||
return queue.offer(instance);
|
||||
}
|
||||
};
|
||||
|
||||
@Nullable
|
||||
static InputEventRunnable obtain() {
|
||||
InputEventRunnable runnable = POOL.acquire();
|
||||
if (runnable == null) {
|
||||
Log.w(TAG, "Input event pool is at capacity");
|
||||
}
|
||||
return runnable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to track when this instance was created and added to the pool. Primarily used for
|
||||
* debug purposes.
|
||||
*/
|
||||
private final int creationRank;
|
||||
|
||||
private InputEventRunnable(int creationRank) {
|
||||
this.creationRank = creationRank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of supported input events.
|
||||
*/
|
||||
private enum EventType {
|
||||
MOUSE,
|
||||
TOUCH,
|
||||
MAGNIFY,
|
||||
PAN,
|
||||
JOYSTICK_BUTTON,
|
||||
JOYSTICK_AXIS,
|
||||
JOYSTICK_HAT,
|
||||
JOYSTICK_CONNECTION_CHANGED,
|
||||
KEY,
|
||||
SENSOR
|
||||
}
|
||||
|
||||
private EventType currentEventType = null;
|
||||
|
||||
// common event fields
|
||||
private float eventX;
|
||||
private float eventY;
|
||||
private float eventDeltaX;
|
||||
private float eventDeltaY;
|
||||
private boolean eventPressed;
|
||||
|
||||
// common touch / mouse fields
|
||||
private int eventAction;
|
||||
private boolean doubleTap;
|
||||
|
||||
// Mouse event fields and setter
|
||||
private int buttonsMask;
|
||||
private boolean sourceMouseRelative;
|
||||
private float pressure;
|
||||
private float tiltX;
|
||||
private float tiltY;
|
||||
void setMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) {
|
||||
this.currentEventType = EventType.MOUSE;
|
||||
this.eventAction = eventAction;
|
||||
this.buttonsMask = buttonsMask;
|
||||
this.eventX = x;
|
||||
this.eventY = y;
|
||||
this.eventDeltaX = deltaX;
|
||||
this.eventDeltaY = deltaY;
|
||||
this.doubleTap = doubleClick;
|
||||
this.sourceMouseRelative = sourceMouseRelative;
|
||||
this.pressure = pressure;
|
||||
this.tiltX = tiltX;
|
||||
this.tiltY = tiltY;
|
||||
}
|
||||
|
||||
// Touch event fields and setter
|
||||
private int actionPointerId;
|
||||
private int pointerCount;
|
||||
private final float[] positions = new float[MAX_TOUCH_POINTER_COUNT * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc...
|
||||
void setTouchEvent(MotionEvent event, int eventAction, boolean doubleTap) {
|
||||
this.currentEventType = EventType.TOUCH;
|
||||
this.eventAction = eventAction;
|
||||
this.doubleTap = doubleTap;
|
||||
this.actionPointerId = event.getPointerId(event.getActionIndex());
|
||||
this.pointerCount = Math.min(event.getPointerCount(), MAX_TOUCH_POINTER_COUNT);
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
positions[i * 6 + 0] = event.getPointerId(i);
|
||||
positions[i * 6 + 1] = event.getX(i);
|
||||
positions[i * 6 + 2] = event.getY(i);
|
||||
positions[i * 6 + 3] = event.getPressure(i);
|
||||
positions[i * 6 + 4] = GodotInputHandler.getEventTiltX(event);
|
||||
positions[i * 6 + 5] = GodotInputHandler.getEventTiltY(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Magnify event fields and setter
|
||||
private float magnifyFactor;
|
||||
void setMagnifyEvent(float x, float y, float factor) {
|
||||
this.currentEventType = EventType.MAGNIFY;
|
||||
this.eventX = x;
|
||||
this.eventY = y;
|
||||
this.magnifyFactor = factor;
|
||||
}
|
||||
|
||||
// Pan event setter
|
||||
void setPanEvent(float x, float y, float deltaX, float deltaY) {
|
||||
this.currentEventType = EventType.PAN;
|
||||
this.eventX = x;
|
||||
this.eventY = y;
|
||||
this.eventDeltaX = deltaX;
|
||||
this.eventDeltaY = deltaY;
|
||||
}
|
||||
|
||||
// common joystick field
|
||||
private int joystickDevice;
|
||||
|
||||
// Joystick button event fields and setter
|
||||
private int button;
|
||||
void setJoystickButtonEvent(int device, int button, boolean pressed) {
|
||||
this.currentEventType = EventType.JOYSTICK_BUTTON;
|
||||
this.joystickDevice = device;
|
||||
this.button = button;
|
||||
this.eventPressed = pressed;
|
||||
}
|
||||
|
||||
// Joystick axis event fields and setter
|
||||
private int axis;
|
||||
private float value;
|
||||
void setJoystickAxisEvent(int device, int axis, float value) {
|
||||
this.currentEventType = EventType.JOYSTICK_AXIS;
|
||||
this.joystickDevice = device;
|
||||
this.axis = axis;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
// Joystick hat event fields and setter
|
||||
private int hatX;
|
||||
private int hatY;
|
||||
void setJoystickHatEvent(int device, int hatX, int hatY) {
|
||||
this.currentEventType = EventType.JOYSTICK_HAT;
|
||||
this.joystickDevice = device;
|
||||
this.hatX = hatX;
|
||||
this.hatY = hatY;
|
||||
}
|
||||
|
||||
// Joystick connection changed event fields and setter
|
||||
private boolean connected;
|
||||
private String joystickName;
|
||||
void setJoystickConnectionChangedEvent(int device, boolean connected, String name) {
|
||||
this.currentEventType = EventType.JOYSTICK_CONNECTION_CHANGED;
|
||||
this.joystickDevice = device;
|
||||
this.connected = connected;
|
||||
this.joystickName = name;
|
||||
}
|
||||
|
||||
// Key event fields and setter
|
||||
private int physicalKeycode;
|
||||
private int unicode;
|
||||
private int keyLabel;
|
||||
private boolean echo;
|
||||
void setKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) {
|
||||
this.currentEventType = EventType.KEY;
|
||||
this.physicalKeycode = physicalKeycode;
|
||||
this.unicode = unicode;
|
||||
this.keyLabel = keyLabel;
|
||||
this.eventPressed = pressed;
|
||||
this.echo = echo;
|
||||
}
|
||||
|
||||
// Sensor event fields and setter
|
||||
private int sensorType;
|
||||
private float rotatedValue0;
|
||||
private float rotatedValue1;
|
||||
private float rotatedValue2;
|
||||
void setSensorEvent(int sensorType, float rotatedValue0, float rotatedValue1, float rotatedValue2) {
|
||||
this.currentEventType = EventType.SENSOR;
|
||||
this.sensorType = sensorType;
|
||||
this.rotatedValue0 = rotatedValue0;
|
||||
this.rotatedValue1 = rotatedValue1;
|
||||
this.rotatedValue2 = rotatedValue2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (currentEventType == null) {
|
||||
Log.w(TAG, "Invalid event type");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (currentEventType) {
|
||||
case MOUSE:
|
||||
GodotLib.dispatchMouseEvent(
|
||||
eventAction,
|
||||
buttonsMask,
|
||||
eventX,
|
||||
eventY,
|
||||
eventDeltaX,
|
||||
eventDeltaY,
|
||||
doubleTap,
|
||||
sourceMouseRelative,
|
||||
pressure,
|
||||
tiltX,
|
||||
tiltY);
|
||||
break;
|
||||
|
||||
case TOUCH:
|
||||
GodotLib.dispatchTouchEvent(
|
||||
eventAction,
|
||||
actionPointerId,
|
||||
pointerCount,
|
||||
positions,
|
||||
doubleTap);
|
||||
break;
|
||||
|
||||
case MAGNIFY:
|
||||
GodotLib.magnify(eventX, eventY, magnifyFactor);
|
||||
break;
|
||||
|
||||
case PAN:
|
||||
GodotLib.pan(eventX, eventY, eventDeltaX, eventDeltaY);
|
||||
break;
|
||||
|
||||
case JOYSTICK_BUTTON:
|
||||
GodotLib.joybutton(joystickDevice, button, eventPressed);
|
||||
break;
|
||||
|
||||
case JOYSTICK_AXIS:
|
||||
GodotLib.joyaxis(joystickDevice, axis, value);
|
||||
break;
|
||||
|
||||
case JOYSTICK_HAT:
|
||||
GodotLib.joyhat(joystickDevice, hatX, hatY);
|
||||
break;
|
||||
|
||||
case JOYSTICK_CONNECTION_CHANGED:
|
||||
GodotLib.joyconnectionchanged(joystickDevice, connected, joystickName);
|
||||
break;
|
||||
|
||||
case KEY:
|
||||
GodotLib.key(physicalKeycode, unicode, keyLabel, eventPressed, echo);
|
||||
break;
|
||||
|
||||
case SENSOR:
|
||||
switch (sensorType) {
|
||||
case Sensor.TYPE_ACCELEROMETER:
|
||||
GodotLib.accelerometer(-rotatedValue0, -rotatedValue1, -rotatedValue2);
|
||||
break;
|
||||
|
||||
case Sensor.TYPE_GRAVITY:
|
||||
GodotLib.gravity(-rotatedValue0, -rotatedValue1, -rotatedValue2);
|
||||
break;
|
||||
|
||||
case Sensor.TYPE_MAGNETIC_FIELD:
|
||||
GodotLib.magnetometer(-rotatedValue0, -rotatedValue1, -rotatedValue2);
|
||||
break;
|
||||
|
||||
case Sensor.TYPE_GYROSCOPE:
|
||||
GodotLib.gyroscope(rotatedValue0, rotatedValue1, rotatedValue2);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the current instance back to the pool
|
||||
*/
|
||||
private void recycle() {
|
||||
currentEventType = null;
|
||||
POOL.release(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
/**************************************************************************/
|
||||
/* FilePicker.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.io
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.DocumentsContract
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.godotengine.godot.GodotLib
|
||||
import org.godotengine.godot.io.file.MediaStoreData
|
||||
|
||||
/**
|
||||
* Utility class for managing file selection and file picker activities.
|
||||
*
|
||||
* It provides methods to launch a file picker and handle the result, supporting various file modes,
|
||||
* including opening files, directories, and saving files.
|
||||
*/
|
||||
internal class FilePicker {
|
||||
companion object {
|
||||
private const val FILE_PICKER_REQUEST = 1000
|
||||
private val TAG = FilePicker::class.java.simpleName
|
||||
|
||||
// Constants for fileMode values
|
||||
private const val FILE_MODE_OPEN_FILE = 0
|
||||
private const val FILE_MODE_OPEN_FILES = 1
|
||||
private const val FILE_MODE_OPEN_DIR = 2
|
||||
private const val FILE_MODE_OPEN_ANY = 3
|
||||
private const val FILE_MODE_SAVE_FILE = 4
|
||||
|
||||
/**
|
||||
* Handles the result from a file picker activity and processes the selected file(s) or directory.
|
||||
*
|
||||
* @param context The context from which the file picker was launched.
|
||||
* @param requestCode The request code used when starting the file picker activity.
|
||||
* @param resultCode The result code returned by the activity.
|
||||
* @param data The intent data containing the selected file(s) or directory.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
fun handleActivityResult(context: Context, requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == FILE_PICKER_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
Log.d(TAG, "File picker canceled")
|
||||
GodotLib.filePickerCallback(false, emptyArray())
|
||||
return
|
||||
}
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val selectedPaths: MutableList<String> = mutableListOf()
|
||||
// Handle multiple file selection.
|
||||
val clipData = data?.clipData
|
||||
if (clipData != null) {
|
||||
for (i in 0 until clipData.itemCount) {
|
||||
val uri = clipData.getItemAt(i).uri
|
||||
uri?.let {
|
||||
val filepath = MediaStoreData.getFilePathFromUri(context, uri)
|
||||
if (filepath != null) {
|
||||
selectedPaths.add(filepath)
|
||||
} else {
|
||||
Log.d(TAG, "null filepath URI: $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val uri: Uri? = data?.data
|
||||
uri?.let {
|
||||
val filepath = MediaStoreData.getFilePathFromUri(context, uri)
|
||||
if (filepath != null) {
|
||||
selectedPaths.add(filepath)
|
||||
} else {
|
||||
Log.d(TAG, "null filepath URI: $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedPaths.isNotEmpty()) {
|
||||
GodotLib.filePickerCallback(true, selectedPaths.toTypedArray())
|
||||
} else {
|
||||
GodotLib.filePickerCallback(false, emptyArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a file picker activity with specified settings based on the mode, initial directory,
|
||||
* file type filters, and other parameters.
|
||||
*
|
||||
* @param context The context from which to start the file picker.
|
||||
* @param activity The activity instance used to initiate the picker. Required for activity results.
|
||||
* @param currentDirectory The directory path to start the file picker in.
|
||||
* @param filename The name of the file when using save mode.
|
||||
* @param fileMode The mode to operate in, specifying open, save, or directory select.
|
||||
* @param filters Array of MIME types to filter file selection.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
fun showFilePicker(context: Context, activity: Activity?, currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) {
|
||||
val intent = when (fileMode) {
|
||||
FILE_MODE_OPEN_DIR -> Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
FILE_MODE_SAVE_FILE -> Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
else -> Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
}
|
||||
val initialDirectory = MediaStoreData.getUriFromDirectoryPath(context, currentDirectory)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && initialDirectory != null) {
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialDirectory)
|
||||
} else {
|
||||
Log.d(TAG, "Error cannot set initial directory")
|
||||
}
|
||||
if (fileMode == FILE_MODE_OPEN_FILES) {
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) // Set multi select for FILE_MODE_OPEN_FILES
|
||||
} else if (fileMode == FILE_MODE_SAVE_FILE) {
|
||||
intent.putExtra(Intent.EXTRA_TITLE, filename) // Set filename for FILE_MODE_SAVE_FILE
|
||||
}
|
||||
// ACTION_OPEN_DOCUMENT_TREE does not support intent type
|
||||
if (fileMode != FILE_MODE_OPEN_DIR) {
|
||||
val resolvedFilters = filters.map { resolveMimeType(it) }.distinct()
|
||||
intent.type = resolvedFilters.firstOrNull { it != "application/octet-stream" } ?: "*/*"
|
||||
if (resolvedFilters.size > 1) {
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, resolvedFilters.toTypedArray())
|
||||
}
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
}
|
||||
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true)
|
||||
activity?.startActivityForResult(intent, FILE_PICKER_REQUEST)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the MIME type for a given file extension.
|
||||
*
|
||||
* @param ext the extension whose MIME type is to be determined.
|
||||
* @return the MIME type as a string, or "application/octet-stream" if the type is unknown.
|
||||
*/
|
||||
private fun resolveMimeType(ext: String): String {
|
||||
val mimeTypeMap = MimeTypeMap.getSingleton()
|
||||
var input = ext
|
||||
|
||||
// Fix for extensions like "*.txt" or ".txt".
|
||||
if (ext.contains(".")) {
|
||||
input = ext.substring(ext.indexOf(".") + 1);
|
||||
}
|
||||
|
||||
// Check if the input is already a valid MIME type.
|
||||
if (mimeTypeMap.hasMimeType(input)) {
|
||||
return input
|
||||
}
|
||||
|
||||
val resolvedMimeType = mimeTypeMap.getMimeTypeFromExtension(input)
|
||||
if (resolvedMimeType != null) {
|
||||
return resolvedMimeType
|
||||
}
|
||||
// Check for wildcard MIME types like "image/*".
|
||||
if (input.contains("/*")) {
|
||||
val category = input.substringBefore("/*")
|
||||
return when (category) {
|
||||
"image" -> "image/*"
|
||||
"video" -> "video/*"
|
||||
"audio" -> "audio/*"
|
||||
else -> "application/octet-stream"
|
||||
}
|
||||
}
|
||||
// Fallback to a generic MIME type if the input is neither a valid extension nor MIME type.
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,11 +34,17 @@ import android.content.Context
|
|||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import java.io.File
|
||||
import org.godotengine.godot.GodotLib
|
||||
|
||||
/**
|
||||
* Represents the different storage scopes.
|
||||
*/
|
||||
internal enum class StorageScope {
|
||||
/**
|
||||
* Covers the 'assets' directory
|
||||
*/
|
||||
ASSETS,
|
||||
|
||||
/**
|
||||
* Covers internal and external directories accessible to the app without restrictions.
|
||||
*/
|
||||
|
|
@ -56,6 +62,10 @@ internal enum class StorageScope {
|
|||
|
||||
class Identifier(context: Context) {
|
||||
|
||||
companion object {
|
||||
internal const val ASSETS_PREFIX = "assets://"
|
||||
}
|
||||
|
||||
private val internalAppDir: String? = context.filesDir.canonicalPath
|
||||
private val internalCacheDir: String? = context.cacheDir.canonicalPath
|
||||
private val externalAppDir: String? = context.getExternalFilesDir(null)?.canonicalPath
|
||||
|
|
@ -63,6 +73,14 @@ internal enum class StorageScope {
|
|||
private val downloadsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath
|
||||
private val documentsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath
|
||||
|
||||
/**
|
||||
* Determine if the given path is accessible.
|
||||
*/
|
||||
fun canAccess(path: String?): Boolean {
|
||||
val storageScope = identifyStorageScope(path)
|
||||
return storageScope == APP || storageScope == SHARED
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which [StorageScope] the given path falls under.
|
||||
*/
|
||||
|
|
@ -71,9 +89,16 @@ internal enum class StorageScope {
|
|||
return UNKNOWN
|
||||
}
|
||||
|
||||
val pathFile = File(path)
|
||||
if (path.startsWith(ASSETS_PREFIX)) {
|
||||
return ASSETS
|
||||
}
|
||||
|
||||
var pathFile = File(path)
|
||||
if (!pathFile.isAbsolute) {
|
||||
return UNKNOWN
|
||||
pathFile = File(GodotLib.getProjectResourceDir(), path)
|
||||
if (!pathFile.isAbsolute) {
|
||||
return UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
// If we have 'All Files Access' permission, we can access all directories without
|
||||
|
|
|
|||
|
|
@ -33,18 +33,30 @@ package org.godotengine.godot.io.directory
|
|||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import org.godotengine.godot.io.StorageScope
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
|
||||
import org.godotengine.godot.io.file.AssetData
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Handles directories access within the Android assets directory.
|
||||
*/
|
||||
internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.DirectoryAccess {
|
||||
internal class AssetsDirectoryAccess(private val context: Context) : DirectoryAccessHandler.DirectoryAccess {
|
||||
|
||||
companion object {
|
||||
private val TAG = AssetsDirectoryAccess::class.java.simpleName
|
||||
|
||||
internal fun getAssetsPath(originalPath: String): String {
|
||||
if (originalPath.startsWith(File.separator)) {
|
||||
return originalPath.substring(File.separator.length)
|
||||
}
|
||||
if (originalPath.startsWith(StorageScope.Identifier.ASSETS_PREFIX)) {
|
||||
return originalPath.substring(StorageScope.Identifier.ASSETS_PREFIX.length)
|
||||
}
|
||||
return originalPath
|
||||
}
|
||||
}
|
||||
|
||||
private data class AssetDir(val path: String, val files: Array<String>, var current: Int = 0)
|
||||
|
|
@ -54,13 +66,6 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
|
|||
private var lastDirId = STARTING_DIR_ID
|
||||
private val dirs = SparseArray<AssetDir>()
|
||||
|
||||
private fun getAssetsPath(originalPath: String): String {
|
||||
if (originalPath.startsWith(File.separatorChar)) {
|
||||
return originalPath.substring(1)
|
||||
}
|
||||
return originalPath
|
||||
}
|
||||
|
||||
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
|
||||
|
||||
override fun dirOpen(path: String): Int {
|
||||
|
|
@ -68,8 +73,8 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
|
|||
try {
|
||||
val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID
|
||||
// Empty directories don't get added to the 'assets' directory, so
|
||||
// if ad.files.length > 0 ==> path is directory
|
||||
// if ad.files.length == 0 ==> path is file
|
||||
// if files.length > 0 ==> path is directory
|
||||
// if files.length == 0 ==> path is file
|
||||
if (files.isEmpty()) {
|
||||
return INVALID_DIR_ID
|
||||
}
|
||||
|
|
@ -89,8 +94,8 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
|
|||
try {
|
||||
val files = assetManager.list(assetsPath) ?: return false
|
||||
// Empty directories don't get added to the 'assets' directory, so
|
||||
// if ad.files.length > 0 ==> path is directory
|
||||
// if ad.files.length == 0 ==> path is file
|
||||
// if files.length > 0 ==> path is directory
|
||||
// if files.length == 0 ==> path is file
|
||||
return files.isNotEmpty()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception on dirExists", e)
|
||||
|
|
@ -98,19 +103,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
|
|||
}
|
||||
}
|
||||
|
||||
override fun fileExists(path: String): Boolean {
|
||||
val assetsPath = getAssetsPath(path)
|
||||
try {
|
||||
val files = assetManager.list(assetsPath) ?: return false
|
||||
// Empty directories don't get added to the 'assets' directory, so
|
||||
// if ad.files.length > 0 ==> path is directory
|
||||
// if ad.files.length == 0 ==> path is file
|
||||
return files.isEmpty()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception on fileExists", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
override fun fileExists(path: String) = AssetData.fileExists(context, path)
|
||||
|
||||
override fun dirIsDir(dirId: Int): Boolean {
|
||||
val ad: AssetDir = dirs[dirId]
|
||||
|
|
@ -171,7 +164,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
|
|||
|
||||
override fun getSpaceLeft() = 0L
|
||||
|
||||
override fun rename(from: String, to: String) = false
|
||||
override fun rename(from: String, to: String) = AssetData.rename(from, to)
|
||||
|
||||
override fun remove(filename: String) = false
|
||||
override fun remove(filename: String) = AssetData.delete(filename)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ package org.godotengine.godot.io.directory
|
|||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM
|
||||
import org.godotengine.godot.Godot
|
||||
import org.godotengine.godot.io.StorageScope
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES
|
||||
|
||||
/**
|
||||
|
|
@ -45,18 +46,82 @@ class DirectoryAccessHandler(context: Context) {
|
|||
|
||||
internal const val INVALID_DIR_ID = -1
|
||||
internal const val STARTING_DIR_ID = 1
|
||||
|
||||
private fun getAccessTypeFromNative(accessType: Int): AccessType? {
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES.nativeValue -> ACCESS_RESOURCES
|
||||
ACCESS_FILESYSTEM.nativeValue -> ACCESS_FILESYSTEM
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class AccessType(val nativeValue: Int) {
|
||||
ACCESS_RESOURCES(0), ACCESS_FILESYSTEM(2)
|
||||
ACCESS_RESOURCES(0),
|
||||
|
||||
/**
|
||||
* Maps to [ACCESS_FILESYSTEM]
|
||||
*/
|
||||
ACCESS_USERDATA(1),
|
||||
ACCESS_FILESYSTEM(2);
|
||||
|
||||
fun generateDirAccessId(dirId: Int) = (dirId * DIR_ACCESS_ID_MULTIPLIER) + nativeValue
|
||||
|
||||
companion object {
|
||||
const val DIR_ACCESS_ID_MULTIPLIER = 10
|
||||
|
||||
fun fromDirAccessId(dirAccessId: Int): Pair<AccessType?, Int> {
|
||||
val nativeValue = dirAccessId % DIR_ACCESS_ID_MULTIPLIER
|
||||
val dirId = dirAccessId / DIR_ACCESS_ID_MULTIPLIER
|
||||
return Pair(fromNative(nativeValue), dirId)
|
||||
}
|
||||
|
||||
private fun fromNative(nativeAccessType: Int): AccessType? {
|
||||
for (accessType in entries) {
|
||||
if (accessType.nativeValue == nativeAccessType) {
|
||||
return accessType
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun fromNative(nativeAccessType: Int, storageScope: StorageScope? = null): AccessType? {
|
||||
val accessType = fromNative(nativeAccessType)
|
||||
if (accessType == null) {
|
||||
Log.w(TAG, "Unsupported access type $nativeAccessType")
|
||||
return null
|
||||
}
|
||||
|
||||
// 'Resources' access type takes precedence as it is simple to handle:
|
||||
// if we receive a 'Resources' access type and this is a template build,
|
||||
// we provide a 'Resources' directory handler.
|
||||
// If this is an editor build, 'Resources' refers to the opened project resources
|
||||
// and so we provide a 'Filesystem' directory handler.
|
||||
if (accessType == ACCESS_RESOURCES) {
|
||||
return if (Godot.isEditorBuild()) {
|
||||
ACCESS_FILESYSTEM
|
||||
} else {
|
||||
ACCESS_RESOURCES
|
||||
}
|
||||
} else {
|
||||
// We've received a 'Filesystem' or 'Userdata' access type. On Android, this
|
||||
// may refer to:
|
||||
// - assets directory (path has 'assets:/' prefix)
|
||||
// - app directories
|
||||
// - device shared directories
|
||||
// As such we check the storage scope (if available) to figure what type of
|
||||
// directory handler to provide
|
||||
if (storageScope != null) {
|
||||
val accessTypeFromStorageScope = when (storageScope) {
|
||||
StorageScope.ASSETS -> ACCESS_RESOURCES
|
||||
StorageScope.APP, StorageScope.SHARED -> ACCESS_FILESYSTEM
|
||||
StorageScope.UNKNOWN -> null
|
||||
}
|
||||
|
||||
if (accessTypeFromStorageScope != null) {
|
||||
return accessTypeFromStorageScope
|
||||
}
|
||||
}
|
||||
// If we're not able to infer the type of directory handler from the storage
|
||||
// scope, we fall-back to the 'Filesystem' directory handler as it's the default
|
||||
// for the 'Filesystem' access type.
|
||||
// Note that ACCESS_USERDATA also maps to ACCESS_FILESYSTEM
|
||||
return ACCESS_FILESYSTEM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal interface DirectoryAccess {
|
||||
|
|
@ -76,8 +141,10 @@ class DirectoryAccessHandler(context: Context) {
|
|||
fun remove(filename: String): Boolean
|
||||
}
|
||||
|
||||
private val storageScopeIdentifier = StorageScope.Identifier(context)
|
||||
|
||||
private val assetsDirAccess = AssetsDirectoryAccess(context)
|
||||
private val fileSystemDirAccess = FilesystemDirectoryAccess(context)
|
||||
private val fileSystemDirAccess = FilesystemDirectoryAccess(context, storageScopeIdentifier)
|
||||
|
||||
fun assetsFileExists(assetsPath: String) = assetsDirAccess.fileExists(assetsPath)
|
||||
fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path)
|
||||
|
|
@ -85,24 +152,32 @@ class DirectoryAccessHandler(context: Context) {
|
|||
private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId)
|
||||
else -> fileSystemDirAccess.hasDirId(dirId)
|
||||
}
|
||||
}
|
||||
|
||||
fun dirOpen(nativeAccessType: Int, path: String?): Int {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType)
|
||||
if (path == null || accessType == null) {
|
||||
if (path == null) {
|
||||
return INVALID_DIR_ID
|
||||
}
|
||||
|
||||
return when (accessType) {
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return INVALID_DIR_ID
|
||||
|
||||
val dirId = when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path)
|
||||
else -> fileSystemDirAccess.dirOpen(path)
|
||||
}
|
||||
if (dirId == INVALID_DIR_ID) {
|
||||
return INVALID_DIR_ID
|
||||
}
|
||||
|
||||
val dirAccessId = accessType.generateDirAccessId(dirId)
|
||||
return dirAccessId
|
||||
}
|
||||
|
||||
fun dirNext(nativeAccessType: Int, dirId: Int): String {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType)
|
||||
fun dirNext(dirAccessId: Int): String {
|
||||
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
|
||||
if (accessType == null || !hasDirId(accessType, dirId)) {
|
||||
Log.w(TAG, "dirNext: Invalid dir id: $dirId")
|
||||
return ""
|
||||
|
|
@ -110,12 +185,12 @@ class DirectoryAccessHandler(context: Context) {
|
|||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId)
|
||||
else -> fileSystemDirAccess.dirNext(dirId)
|
||||
}
|
||||
}
|
||||
|
||||
fun dirClose(nativeAccessType: Int, dirId: Int) {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType)
|
||||
fun dirClose(dirAccessId: Int) {
|
||||
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
|
||||
if (accessType == null || !hasDirId(accessType, dirId)) {
|
||||
Log.w(TAG, "dirClose: Invalid dir id: $dirId")
|
||||
return
|
||||
|
|
@ -123,12 +198,12 @@ class DirectoryAccessHandler(context: Context) {
|
|||
|
||||
when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId)
|
||||
else -> fileSystemDirAccess.dirClose(dirId)
|
||||
}
|
||||
}
|
||||
|
||||
fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType)
|
||||
fun dirIsDir(dirAccessId: Int): Boolean {
|
||||
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
|
||||
if (accessType == null || !hasDirId(accessType, dirId)) {
|
||||
Log.w(TAG, "dirIsDir: Invalid dir id: $dirId")
|
||||
return false
|
||||
|
|
@ -136,91 +211,106 @@ class DirectoryAccessHandler(context: Context) {
|
|||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId)
|
||||
else -> fileSystemDirAccess.dirIsDir(dirId)
|
||||
}
|
||||
}
|
||||
|
||||
fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType)
|
||||
fun isCurrentHidden(dirAccessId: Int): Boolean {
|
||||
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
|
||||
if (accessType == null || !hasDirId(accessType, dirId)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId)
|
||||
else -> fileSystemDirAccess.isCurrentHidden(dirId)
|
||||
}
|
||||
}
|
||||
|
||||
fun dirExists(nativeAccessType: Int, path: String?): Boolean {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType)
|
||||
if (path == null || accessType == null) {
|
||||
if (path == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
|
||||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.dirExists(path)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path)
|
||||
else -> fileSystemDirAccess.dirExists(path)
|
||||
}
|
||||
}
|
||||
|
||||
fun fileExists(nativeAccessType: Int, path: String?): Boolean {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType)
|
||||
if (path == null || accessType == null) {
|
||||
if (path == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
|
||||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.fileExists(path)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path)
|
||||
else -> fileSystemDirAccess.fileExists(path)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDriveCount(nativeAccessType: Int): Int {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0
|
||||
val accessType = AccessType.fromNative(nativeAccessType) ?: return 0
|
||||
return when(accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.getDriveCount()
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount()
|
||||
else -> fileSystemDirAccess.getDriveCount()
|
||||
}
|
||||
}
|
||||
|
||||
fun getDrive(nativeAccessType: Int, drive: Int): String {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return ""
|
||||
val accessType = AccessType.fromNative(nativeAccessType) ?: return ""
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive)
|
||||
else -> fileSystemDirAccess.getDrive(drive)
|
||||
}
|
||||
}
|
||||
|
||||
fun makeDir(nativeAccessType: Int, dir: String): Boolean {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
|
||||
fun makeDir(nativeAccessType: Int, dir: String?): Boolean {
|
||||
if (dir == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(dir)
|
||||
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
|
||||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir)
|
||||
else -> fileSystemDirAccess.makeDir(dir)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSpaceLeft(nativeAccessType: Int): Long {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L
|
||||
val accessType = AccessType.fromNative(nativeAccessType) ?: return 0L
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft()
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft()
|
||||
else -> fileSystemDirAccess.getSpaceLeft()
|
||||
}
|
||||
}
|
||||
|
||||
fun rename(nativeAccessType: Int, from: String, to: String): Boolean {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
|
||||
val accessType = AccessType.fromNative(nativeAccessType) ?: return false
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.rename(from, to)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to)
|
||||
else -> fileSystemDirAccess.rename(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(nativeAccessType: Int, filename: String): Boolean {
|
||||
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
|
||||
fun remove(nativeAccessType: Int, filename: String?): Boolean {
|
||||
if (filename == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(filename)
|
||||
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.remove(filename)
|
||||
ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename)
|
||||
else -> fileSystemDirAccess.remove(filename)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import java.io.File
|
|||
/**
|
||||
* Handles directories access with the internal and external filesystem.
|
||||
*/
|
||||
internal class FilesystemDirectoryAccess(private val context: Context):
|
||||
internal class FilesystemDirectoryAccess(private val context: Context, private val storageScopeIdentifier: StorageScope.Identifier):
|
||||
DirectoryAccessHandler.DirectoryAccess {
|
||||
|
||||
companion object {
|
||||
|
|
@ -54,7 +54,6 @@ internal class FilesystemDirectoryAccess(private val context: Context):
|
|||
|
||||
private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0)
|
||||
|
||||
private val storageScopeIdentifier = StorageScope.Identifier(context)
|
||||
private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||
private var lastDirId = STARTING_DIR_ID
|
||||
private val dirs = SparseArray<DirData>()
|
||||
|
|
@ -63,7 +62,8 @@ internal class FilesystemDirectoryAccess(private val context: Context):
|
|||
// Directory access is available for shared storage on Android 11+
|
||||
// On Android 10, access is also available as long as the `requestLegacyExternalStorage`
|
||||
// tag is available.
|
||||
return storageScopeIdentifier.identifyStorageScope(path) != StorageScope.UNKNOWN
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
return storageScope != StorageScope.UNKNOWN && storageScope != StorageScope.ASSETS
|
||||
}
|
||||
|
||||
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,152 @@
|
|||
/**************************************************************************/
|
||||
/* AssetData.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.io.file
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.AssetManager
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.io.directory.AssetsDirectoryAccess
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.ReadableByteChannel
|
||||
|
||||
/**
|
||||
* Implementation of the [DataAccess] which handles access and interaction with files in the
|
||||
* 'assets' directory
|
||||
*/
|
||||
internal class AssetData(context: Context, private val filePath: String, accessFlag: FileAccessFlags) : DataAccess() {
|
||||
|
||||
companion object {
|
||||
private val TAG = AssetData::class.java.simpleName
|
||||
|
||||
fun fileExists(context: Context, path: String): Boolean {
|
||||
val assetsPath = AssetsDirectoryAccess.getAssetsPath(path)
|
||||
try {
|
||||
val files = context.assets.list(assetsPath) ?: return false
|
||||
// Empty directories don't get added to the 'assets' directory, so
|
||||
// if files.length > 0 ==> path is directory
|
||||
// if files.length == 0 ==> path is file
|
||||
return files.isEmpty()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception on fileExists", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun fileLastModified(path: String) = 0L
|
||||
|
||||
fun delete(path: String) = false
|
||||
|
||||
fun rename(from: String, to: String) = false
|
||||
}
|
||||
|
||||
private val inputStream: InputStream
|
||||
internal val readChannel: ReadableByteChannel
|
||||
|
||||
private var position = 0L
|
||||
private val length: Long
|
||||
|
||||
init {
|
||||
if (accessFlag == FileAccessFlags.WRITE) {
|
||||
throw UnsupportedOperationException("Writing to the 'assets' directory is not supported")
|
||||
}
|
||||
|
||||
val assetsPath = AssetsDirectoryAccess.getAssetsPath(filePath)
|
||||
inputStream = context.assets.open(assetsPath, AssetManager.ACCESS_BUFFER)
|
||||
readChannel = Channels.newChannel(inputStream)
|
||||
|
||||
length = inputStream.available().toLong()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
try {
|
||||
inputStream.close()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when closing file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun flush() {
|
||||
Log.w(TAG, "flush() is not supported.")
|
||||
}
|
||||
|
||||
override fun seek(position: Long) {
|
||||
try {
|
||||
inputStream.skip(position)
|
||||
|
||||
this.position = position
|
||||
if (this.position > length) {
|
||||
this.position = length
|
||||
endOfFile = true
|
||||
} else {
|
||||
endOfFile = false
|
||||
}
|
||||
|
||||
} catch(e: IOException) {
|
||||
Log.w(TAG, "Exception when seeking file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resize(length: Long): Error {
|
||||
Log.w(TAG, "resize() is not supported.")
|
||||
return Error.ERR_UNAVAILABLE
|
||||
}
|
||||
|
||||
override fun position() = position
|
||||
|
||||
override fun size() = length
|
||||
|
||||
override fun read(buffer: ByteBuffer): Int {
|
||||
return try {
|
||||
val readBytes = readChannel.read(buffer)
|
||||
if (readBytes == -1) {
|
||||
endOfFile = true
|
||||
0
|
||||
} else {
|
||||
position += readBytes
|
||||
endOfFile = position() >= size()
|
||||
readBytes
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception while reading from $filePath.", e)
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer): Boolean {
|
||||
Log.w(TAG, "write() is not supported.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -33,12 +33,17 @@ package org.godotengine.godot.io.file
|
|||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.io.StorageScope
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.ClosedChannelException
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.NonWritableChannelException
|
||||
import kotlin.jvm.Throws
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
|
|
@ -47,11 +52,37 @@ import kotlin.math.max
|
|||
* Its derived instances provide concrete implementations to handle regular file access, as well
|
||||
* as file access through the media store API on versions of Android were scoped storage is enabled.
|
||||
*/
|
||||
internal abstract class DataAccess(private val filePath: String) {
|
||||
internal abstract class DataAccess {
|
||||
|
||||
companion object {
|
||||
private val TAG = DataAccess::class.java.simpleName
|
||||
|
||||
@Throws(java.lang.Exception::class, FileNotFoundException::class)
|
||||
fun getInputStream(storageScope: StorageScope, context: Context, filePath: String): InputStream? {
|
||||
return when(storageScope) {
|
||||
StorageScope.ASSETS -> {
|
||||
val assetData = AssetData(context, filePath, FileAccessFlags.READ)
|
||||
Channels.newInputStream(assetData.readChannel)
|
||||
}
|
||||
|
||||
StorageScope.APP -> {
|
||||
val fileData = FileData(filePath, FileAccessFlags.READ)
|
||||
Channels.newInputStream(fileData.fileChannel)
|
||||
}
|
||||
StorageScope.SHARED -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val mediaStoreData = MediaStoreData(context, filePath, FileAccessFlags.READ)
|
||||
Channels.newInputStream(mediaStoreData.fileChannel)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
StorageScope.UNKNOWN -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(java.lang.Exception::class, FileNotFoundException::class)
|
||||
fun generateDataAccess(
|
||||
storageScope: StorageScope,
|
||||
context: Context,
|
||||
|
|
@ -61,6 +92,8 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||
return when (storageScope) {
|
||||
StorageScope.APP -> FileData(filePath, accessFlag)
|
||||
|
||||
StorageScope.ASSETS -> AssetData(context, filePath, accessFlag)
|
||||
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData(context, filePath, accessFlag)
|
||||
} else {
|
||||
|
|
@ -74,7 +107,13 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||
fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean {
|
||||
return when(storageScope) {
|
||||
StorageScope.APP -> FileData.fileExists(path)
|
||||
StorageScope.SHARED -> MediaStoreData.fileExists(context, path)
|
||||
StorageScope.ASSETS -> AssetData.fileExists(context, path)
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData.fileExists(context, path)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
StorageScope.UNKNOWN -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -82,7 +121,13 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||
fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long {
|
||||
return when(storageScope) {
|
||||
StorageScope.APP -> FileData.fileLastModified(path)
|
||||
StorageScope.SHARED -> MediaStoreData.fileLastModified(context, path)
|
||||
StorageScope.ASSETS -> AssetData.fileLastModified(path)
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData.fileLastModified(context, path)
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
|
||||
StorageScope.UNKNOWN -> 0L
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +135,13 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||
fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean {
|
||||
return when(storageScope) {
|
||||
StorageScope.APP -> FileData.delete(path)
|
||||
StorageScope.SHARED -> MediaStoreData.delete(context, path)
|
||||
StorageScope.ASSETS -> AssetData.delete(path)
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData.delete(context, path)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
StorageScope.UNKNOWN -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -98,103 +149,122 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||
fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean {
|
||||
return when(storageScope) {
|
||||
StorageScope.APP -> FileData.rename(from, to)
|
||||
StorageScope.SHARED -> MediaStoreData.rename(context, from, to)
|
||||
StorageScope.ASSETS -> AssetData.rename(from, to)
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData.rename(context, from, to)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
StorageScope.UNKNOWN -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract val fileChannel: FileChannel
|
||||
internal var endOfFile = false
|
||||
|
||||
fun close() {
|
||||
try {
|
||||
fileChannel.close()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when closing file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun flush() {
|
||||
try {
|
||||
fileChannel.force(false)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when flushing file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun seek(position: Long) {
|
||||
try {
|
||||
fileChannel.position(position)
|
||||
endOfFile = position >= fileChannel.size()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Exception when seeking file $filePath.", e)
|
||||
}
|
||||
}
|
||||
abstract fun close()
|
||||
abstract fun flush()
|
||||
abstract fun seek(position: Long)
|
||||
abstract fun resize(length: Long): Error
|
||||
abstract fun position(): Long
|
||||
abstract fun size(): Long
|
||||
abstract fun read(buffer: ByteBuffer): Int
|
||||
abstract fun write(buffer: ByteBuffer): Boolean
|
||||
|
||||
fun seekFromEnd(positionFromEnd: Long) {
|
||||
val positionFromBeginning = max(0, size() - positionFromEnd)
|
||||
seek(positionFromBeginning)
|
||||
}
|
||||
|
||||
fun resize(length: Long): Int {
|
||||
return try {
|
||||
fileChannel.truncate(length)
|
||||
FileErrors.OK.nativeValue
|
||||
} catch (e: NonWritableChannelException) {
|
||||
FileErrors.FILE_CANT_OPEN.nativeValue
|
||||
} catch (e: ClosedChannelException) {
|
||||
FileErrors.FILE_CANT_OPEN.nativeValue
|
||||
} catch (e: IllegalArgumentException) {
|
||||
FileErrors.INVALID_PARAMETER.nativeValue
|
||||
} catch (e: IOException) {
|
||||
FileErrors.FAILED.nativeValue
|
||||
}
|
||||
}
|
||||
abstract class FileChannelDataAccess(private val filePath: String) : DataAccess() {
|
||||
internal abstract val fileChannel: FileChannel
|
||||
|
||||
fun position(): Long {
|
||||
return try {
|
||||
fileChannel.position()
|
||||
override fun close() {
|
||||
try {
|
||||
fileChannel.close()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when closing file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun flush() {
|
||||
try {
|
||||
fileChannel.force(false)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when flushing file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun seek(position: Long) {
|
||||
try {
|
||||
fileChannel.position(position)
|
||||
endOfFile = position >= fileChannel.size()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Exception when seeking file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resize(length: Long): Error {
|
||||
return try {
|
||||
fileChannel.truncate(length)
|
||||
Error.OK
|
||||
} catch (e: NonWritableChannelException) {
|
||||
Error.ERR_FILE_CANT_OPEN
|
||||
} catch (e: ClosedChannelException) {
|
||||
Error.ERR_FILE_CANT_OPEN
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Error.ERR_INVALID_PARAMETER
|
||||
} catch (e: IOException) {
|
||||
Error.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
override fun position(): Long {
|
||||
return try {
|
||||
fileChannel.position()
|
||||
} catch (e: IOException) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Exception when retrieving position for file $filePath.",
|
||||
e
|
||||
)
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
override fun size() = try {
|
||||
fileChannel.size()
|
||||
} catch (e: IOException) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Exception when retrieving position for file $filePath.",
|
||||
e
|
||||
)
|
||||
Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
fun size() = try {
|
||||
fileChannel.size()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
|
||||
0L
|
||||
}
|
||||
|
||||
fun read(buffer: ByteBuffer): Int {
|
||||
return try {
|
||||
val readBytes = fileChannel.read(buffer)
|
||||
endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size())
|
||||
if (readBytes == -1) {
|
||||
override fun read(buffer: ByteBuffer): Int {
|
||||
return try {
|
||||
val readBytes = fileChannel.read(buffer)
|
||||
endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size())
|
||||
if (readBytes == -1) {
|
||||
0
|
||||
} else {
|
||||
readBytes
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception while reading from file $filePath.", e)
|
||||
0
|
||||
} else {
|
||||
readBytes
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception while reading from file $filePath.", e)
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fun write(buffer: ByteBuffer) {
|
||||
try {
|
||||
val writtenBytes = fileChannel.write(buffer)
|
||||
if (writtenBytes > 0) {
|
||||
endOfFile = false
|
||||
override fun write(buffer: ByteBuffer): Boolean {
|
||||
try {
|
||||
val writtenBytes = fileChannel.write(buffer)
|
||||
if (writtenBytes > 0) {
|
||||
endOfFile = false
|
||||
}
|
||||
return true
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception while writing to file $filePath.", e)
|
||||
return false
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception while writing to file $filePath.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ internal enum class FileAccessFlags(val nativeValue: Int) {
|
|||
|
||||
companion object {
|
||||
fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? {
|
||||
for (flag in values()) {
|
||||
for (flag in entries) {
|
||||
if (flag.nativeValue == modeFlag) {
|
||||
return flag
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,11 @@ package org.godotengine.godot.io.file
|
|||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.io.StorageScope
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
|
|
@ -45,8 +48,20 @@ class FileAccessHandler(val context: Context) {
|
|||
companion object {
|
||||
private val TAG = FileAccessHandler::class.java.simpleName
|
||||
|
||||
internal const val INVALID_FILE_ID = 0
|
||||
private const val INVALID_FILE_ID = 0
|
||||
private const val STARTING_FILE_ID = 1
|
||||
private val FILE_OPEN_FAILED = Pair(Error.FAILED, INVALID_FILE_ID)
|
||||
|
||||
internal fun getInputStream(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): InputStream? {
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
return try {
|
||||
path?.let {
|
||||
DataAccess.getInputStream(storageScope, context, path)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
|
|
@ -92,35 +107,55 @@ class FileAccessHandler(val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private val storageScopeIdentifier = StorageScope.Identifier(context)
|
||||
internal val storageScopeIdentifier = StorageScope.Identifier(context)
|
||||
private val files = SparseArray<DataAccess>()
|
||||
private var lastFileId = STARTING_FILE_ID
|
||||
|
||||
private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0
|
||||
|
||||
fun fileOpen(path: String?, modeFlags: Int): Int {
|
||||
val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID
|
||||
return fileOpen(path, accessFlag)
|
||||
fun canAccess(filePath: String?): Boolean {
|
||||
return storageScopeIdentifier.canAccess(filePath)
|
||||
}
|
||||
|
||||
internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int {
|
||||
/**
|
||||
* Returns a positive (> 0) file id when the operation succeeds.
|
||||
* Otherwise, returns a negative value of [Error].
|
||||
*/
|
||||
fun fileOpen(path: String?, modeFlags: Int): Int {
|
||||
val (fileError, fileId) = fileOpen(path, FileAccessFlags.fromNativeModeFlags(modeFlags))
|
||||
return if (fileError == Error.OK) {
|
||||
fileId
|
||||
} else {
|
||||
// Return the negative of the [Error#toNativeValue()] value to differentiate from the
|
||||
// positive file id.
|
||||
-fileError.toNativeValue()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun fileOpen(path: String?, accessFlag: FileAccessFlags?): Pair<Error, Int> {
|
||||
if (accessFlag == null) {
|
||||
return FILE_OPEN_FAILED
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
if (storageScope == StorageScope.UNKNOWN) {
|
||||
return INVALID_FILE_ID
|
||||
return FILE_OPEN_FAILED
|
||||
}
|
||||
|
||||
return try {
|
||||
path?.let {
|
||||
val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return INVALID_FILE_ID
|
||||
val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return FILE_OPEN_FAILED
|
||||
|
||||
files.put(++lastFileId, dataAccess)
|
||||
lastFileId
|
||||
} ?: INVALID_FILE_ID
|
||||
Pair(Error.OK, lastFileId)
|
||||
} ?: FILE_OPEN_FAILED
|
||||
} catch (e: FileNotFoundException) {
|
||||
FileErrors.FILE_NOT_FOUND.nativeValue
|
||||
Pair(Error.ERR_FILE_NOT_FOUND, INVALID_FILE_ID)
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
Pair(Error.ERR_UNAVAILABLE, INVALID_FILE_ID)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error while opening $path", e)
|
||||
INVALID_FILE_ID
|
||||
FILE_OPEN_FAILED
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,12 +191,12 @@ class FileAccessHandler(val context: Context) {
|
|||
return files[fileId].read(byteBuffer)
|
||||
}
|
||||
|
||||
fun fileWrite(fileId: Int, byteBuffer: ByteBuffer?) {
|
||||
fun fileWrite(fileId: Int, byteBuffer: ByteBuffer?): Boolean {
|
||||
if (!hasFileId(fileId) || byteBuffer == null) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
files[fileId].write(byteBuffer)
|
||||
return files[fileId].write(byteBuffer)
|
||||
}
|
||||
|
||||
fun fileFlush(fileId: Int) {
|
||||
|
|
@ -172,6 +207,10 @@ class FileAccessHandler(val context: Context) {
|
|||
files[fileId].flush()
|
||||
}
|
||||
|
||||
fun getInputStream(path: String?) = Companion.getInputStream(context, storageScopeIdentifier, path)
|
||||
|
||||
fun renameFile(from: String, to: String) = Companion.renameFile(context, storageScopeIdentifier, from, to)
|
||||
|
||||
fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path)
|
||||
|
||||
fun fileLastModified(filepath: String?): Long {
|
||||
|
|
@ -191,10 +230,10 @@ class FileAccessHandler(val context: Context) {
|
|||
|
||||
fun fileResize(fileId: Int, length: Long): Int {
|
||||
if (!hasFileId(fileId)) {
|
||||
return FileErrors.FAILED.nativeValue
|
||||
return Error.FAILED.toNativeValue()
|
||||
}
|
||||
|
||||
return files[fileId].resize(length)
|
||||
return files[fileId].resize(length).toNativeValue()
|
||||
}
|
||||
|
||||
fun fileGetPosition(fileId: Int): Long {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import java.nio.channels.FileChannel
|
|||
/**
|
||||
* Implementation of [DataAccess] which handles regular (not scoped) file access and interactions.
|
||||
*/
|
||||
internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess(filePath) {
|
||||
internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess.FileChannelDataAccess(filePath) {
|
||||
|
||||
companion object {
|
||||
private val TAG = FileData::class.java.simpleName
|
||||
|
|
@ -53,7 +53,7 @@ internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAcc
|
|||
|
||||
fun fileLastModified(filepath: String): Long {
|
||||
return try {
|
||||
File(filepath).lastModified()
|
||||
File(filepath).lastModified() / 1000L
|
||||
} catch (e: SecurityException) {
|
||||
0L
|
||||
}
|
||||
|
|
@ -80,10 +80,16 @@ internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAcc
|
|||
override val fileChannel: FileChannel
|
||||
|
||||
init {
|
||||
if (accessFlag == FileAccessFlags.WRITE) {
|
||||
fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel
|
||||
fileChannel = if (accessFlag == FileAccessFlags.WRITE) {
|
||||
// Create parent directory is necessary
|
||||
val parentDir = File(filePath).parentFile
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs()
|
||||
}
|
||||
|
||||
FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel
|
||||
} else {
|
||||
fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel
|
||||
RandomAccessFile(filePath, accessFlag.getMode()).channel
|
||||
}
|
||||
|
||||
if (accessFlag.shouldTruncate()) {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
import java.io.File
|
||||
|
|
@ -46,13 +47,14 @@ import java.io.FileNotFoundException
|
|||
import java.io.FileOutputStream
|
||||
import java.nio.channels.FileChannel
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of [DataAccess] which handles access and interactions with file and data
|
||||
* under scoped storage via the MediaStore API.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) :
|
||||
DataAccess(filePath) {
|
||||
DataAccess.FileChannelDataAccess(filePath) {
|
||||
|
||||
private data class DataItem(
|
||||
val id: Long,
|
||||
|
|
@ -81,6 +83,10 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi
|
|||
private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " +
|
||||
" AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?"
|
||||
|
||||
private const val AUTHORITY_MEDIA_DOCUMENTS = "com.android.providers.media.documents"
|
||||
private const val AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS = "com.android.externalstorage.documents"
|
||||
private const val AUTHORITY_DOWNLOADS_DOCUMENTS = "com.android.providers.downloads.documents"
|
||||
|
||||
private fun getSelectionByPathArguments(path: String): Array<String> {
|
||||
return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path))
|
||||
}
|
||||
|
|
@ -203,7 +209,7 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi
|
|||
}
|
||||
|
||||
val dataItem = result[0]
|
||||
return dataItem.dateModified.toLong()
|
||||
return dataItem.dateModified.toLong() / 1000L
|
||||
}
|
||||
|
||||
fun rename(context: Context, from: String, to: String): Boolean {
|
||||
|
|
@ -230,6 +236,72 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi
|
|||
)
|
||||
return updated > 0
|
||||
}
|
||||
|
||||
fun getUriFromDirectoryPath(context: Context, directoryPath: String): Uri? {
|
||||
if (!directoryExists(directoryPath)) {
|
||||
return null
|
||||
}
|
||||
// Check if the path is under external storage.
|
||||
val externalStorageRoot = Environment.getExternalStorageDirectory().absolutePath
|
||||
if (directoryPath.startsWith(externalStorageRoot)) {
|
||||
val relativePath = directoryPath.replaceFirst(externalStorageRoot, "").trim('/')
|
||||
val uri = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS)
|
||||
.appendPath("document")
|
||||
.appendPath("primary:$relativePath")
|
||||
.build()
|
||||
return uri
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getFilePathFromUri(context: Context, uri: Uri): String? {
|
||||
// Converts content uri to filepath.
|
||||
val id = getIdFromUri(uri) ?: return null
|
||||
|
||||
if (uri.authority == AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS) {
|
||||
val split = id.split(":")
|
||||
val fileName = split.last()
|
||||
val relativePath = split.dropLast(1).joinToString("/")
|
||||
val fullPath = File(Environment.getExternalStorageDirectory(), "$relativePath/$fileName").absolutePath
|
||||
return fullPath
|
||||
} else {
|
||||
val id = id.toLongOrNull() ?: return null
|
||||
val dataItems = queryById(context, id)
|
||||
return if (dataItems.isNotEmpty()) {
|
||||
val dataItem = dataItems[0]
|
||||
File(Environment.getExternalStorageDirectory(), File(dataItem.relativePath, dataItem.displayName).toString()).absolutePath
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIdFromUri(uri: Uri): String? {
|
||||
return try {
|
||||
if (uri.authority == AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS || uri.authority == AUTHORITY_MEDIA_DOCUMENTS || uri.authority == AUTHORITY_DOWNLOADS_DOCUMENTS) {
|
||||
val documentId = uri.lastPathSegment ?: throw IllegalArgumentException("Invalid URI: $uri")
|
||||
documentId.substringAfter(":")
|
||||
} else {
|
||||
throw IllegalArgumentException("Unsupported URI format: $uri")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to parse ID from URI: $uri", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun directoryExists(path: String): Boolean {
|
||||
return try {
|
||||
val file = File(path)
|
||||
file.isDirectory && file.exists()
|
||||
} catch (e: SecurityException) {
|
||||
Log.d(TAG, "Failed to check directoryExists: $path", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val id: Long
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
/**************************************************************************/
|
||||
/* AndroidRuntimePlugin.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.plugin
|
||||
|
||||
import org.godotengine.godot.Godot
|
||||
import org.godotengine.godot.variant.Callable
|
||||
|
||||
/**
|
||||
* Provides access to the Android runtime capabilities.
|
||||
*
|
||||
* For example, from gdscript, developers can use [getApplicationContext] to access system services
|
||||
* and check if the device supports vibration.
|
||||
*
|
||||
* var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
* if android_runtime:
|
||||
* print("Checking if the device supports vibration")
|
||||
* var vibrator_service = android_runtime.getApplicationContext().getSystemService("vibrator")
|
||||
* if vibrator_service:
|
||||
* if vibrator_service.hasVibrator():
|
||||
* print("Vibration is supported on device!")
|
||||
* else:
|
||||
* printerr("Vibration is not supported on device")
|
||||
* else:
|
||||
* printerr("Unable to retrieve the vibrator service")
|
||||
* else:
|
||||
* printerr("Couldn't find AndroidRuntime singleton")
|
||||
*
|
||||
*
|
||||
* Or it can be used to display an Android native toast from gdscript
|
||||
*
|
||||
* var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
* if android_runtime:
|
||||
* var activity = android_runtime.getActivity()
|
||||
*
|
||||
* var toastCallable = func ():
|
||||
* var ToastClass = JavaClassWrapper.wrap("android.widget.Toast")
|
||||
* ToastClass.makeText(activity, "This is a test", ToastClass.LENGTH_LONG).show()
|
||||
*
|
||||
* activity.runOnUiThread(android_runtime.createRunnableFromGodotCallable(toastCallable))
|
||||
* else:
|
||||
* printerr("Unable to access android runtime")
|
||||
*/
|
||||
class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) {
|
||||
override fun getPluginName() = "AndroidRuntime"
|
||||
|
||||
/**
|
||||
* Provides access to the application context to GDScript
|
||||
*/
|
||||
@UsedByGodot
|
||||
fun getApplicationContext() = activity?.applicationContext
|
||||
|
||||
/**
|
||||
* Provides access to the host activity to GDScript
|
||||
*/
|
||||
@UsedByGodot
|
||||
override fun getActivity() = super.getActivity()
|
||||
|
||||
/**
|
||||
* Utility method used to create [Runnable] from Godot [Callable].
|
||||
*/
|
||||
@UsedByGodot
|
||||
fun createRunnableFromGodotCallable(godotCallable: Callable): Runnable {
|
||||
return Runnable { godotCallable.call() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method used to create [java.util.concurrent.Callable] from Godot [Callable].
|
||||
*/
|
||||
@UsedByGodot
|
||||
fun createCallableFromGodotCallable(godotCallable: Callable): java.util.concurrent.Callable<Any> {
|
||||
return java.util.concurrent.Callable { godotCallable.call() }
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
|
|
@ -82,6 +83,9 @@ public final class GodotPluginRegistry {
|
|||
* Retrieve the full set of loaded plugins.
|
||||
*/
|
||||
public Collection<GodotPlugin> getAllPlugins() {
|
||||
if (registry.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return registry.values();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public final class SignalInfo {
|
|||
}
|
||||
|
||||
this.name = signalName;
|
||||
this.paramTypes = paramTypes == null ? new Class<?>[ 0 ] : paramTypes;
|
||||
this.paramTypes = paramTypes == null ? new Class<?>[0] : paramTypes;
|
||||
this.paramTypesNames = new String[this.paramTypes.length];
|
||||
for (int i = 0; i < this.paramTypes.length; i++) {
|
||||
this.paramTypesNames[i] = this.paramTypes[i].getName();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import android.os.SystemClock
|
|||
import android.os.Trace
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.BuildConfig
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.io.file.FileAccessFlags
|
||||
import org.godotengine.godot.io.file.FileAccessHandler
|
||||
import org.json.JSONObject
|
||||
|
|
@ -128,8 +129,8 @@ fun dumpBenchmark(fileAccessHandler: FileAccessHandler? = null, filepath: String
|
|||
Log.i(TAG, "BENCHMARK:\n$printOut")
|
||||
|
||||
if (fileAccessHandler != null && !filepath.isNullOrBlank()) {
|
||||
val fileId = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE)
|
||||
if (fileId != FileAccessHandler.INVALID_FILE_ID) {
|
||||
val (fileError, fileId) = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE)
|
||||
if (fileError == Error.OK) {
|
||||
val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4)
|
||||
fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray()))
|
||||
fileAccessHandler.fileClose(fileId)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**************************************************************************/
|
||||
/* FileErrors.kt */
|
||||
/* DeviceUtils.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
|
|
@ -28,26 +28,33 @@
|
|||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.file
|
||||
/**
|
||||
* Contains utility methods for detecting specific devices.
|
||||
*/
|
||||
@file:JvmName("DeviceUtils")
|
||||
|
||||
package org.godotengine.godot.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
||||
/**
|
||||
* Set of errors that may occur when performing data access.
|
||||
* Returns true if running on Meta Horizon OS.
|
||||
*/
|
||||
internal enum class FileErrors(val nativeValue: Int) {
|
||||
OK(0),
|
||||
FAILED(-1),
|
||||
FILE_NOT_FOUND(-2),
|
||||
FILE_CANT_OPEN(-3),
|
||||
INVALID_PARAMETER(-4);
|
||||
|
||||
companion object {
|
||||
fun fromNativeError(error: Int): FileErrors? {
|
||||
for (fileError in entries) {
|
||||
if (fileError.nativeValue == error) {
|
||||
return fileError
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
fun isHorizonOSDevice(context: Context): Boolean {
|
||||
return context.packageManager.hasSystemFeature("oculus.hardware.standalone_vr")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if running on PICO OS.
|
||||
*/
|
||||
fun isPicoOSDevice(): Boolean {
|
||||
return ("Pico".equals(Build.BRAND, true))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if running on a native Android XR device.
|
||||
*/
|
||||
fun isNativeXRDevice(context: Context): Boolean {
|
||||
return isHorizonOSDevice(context) || isPicoOSDevice()
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
/**************************************************************************/
|
||||
/* DialogUtils.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.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.TextView
|
||||
|
||||
import org.godotengine.godot.R
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Utility class for managing dialogs.
|
||||
*/
|
||||
class DialogUtils {
|
||||
companion object {
|
||||
private val TAG = DialogUtils::class.java.simpleName
|
||||
|
||||
/**
|
||||
* Invoked on dialog button press.
|
||||
*/
|
||||
@JvmStatic
|
||||
private external fun dialogCallback(buttonIndex: Int)
|
||||
|
||||
/**
|
||||
* Invoked on the input dialog submitted.
|
||||
*/
|
||||
@JvmStatic
|
||||
private external fun inputDialogCallback(text: String)
|
||||
|
||||
/**
|
||||
* Displays a dialog with dynamically arranged buttons based on their text length.
|
||||
*
|
||||
* The buttons are laid out in rows, with a maximum of 2 buttons per row. If a button's text
|
||||
* is too long to fit within half the screen width, it occupies the entire row.
|
||||
*
|
||||
* @param activity The activity where the dialog will be displayed.
|
||||
* @param title The title of the dialog.
|
||||
* @param message The message displayed in the dialog.
|
||||
* @param buttons An array of button labels to display.
|
||||
*/
|
||||
internal fun showDialog(activity: Activity, title: String, message: String, buttons: Array<String>) {
|
||||
var dismissDialog: () -> Unit = {} // Helper to dismiss the Dialog when a button is clicked.
|
||||
activity.runOnUiThread {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setTitle(title)
|
||||
builder.setMessage(message)
|
||||
|
||||
val buttonHeight = activity.resources.getDimensionPixelSize(R.dimen.button_height)
|
||||
val paddingHorizontal = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_horizontal)
|
||||
val paddingVertical = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_vertical)
|
||||
val buttonPadding = activity.resources.getDimensionPixelSize(R.dimen.button_padding)
|
||||
|
||||
// Create a vertical parent layout to hold all rows of buttons.
|
||||
val parentLayout = LinearLayout(activity)
|
||||
parentLayout.orientation = LinearLayout.VERTICAL
|
||||
parentLayout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
||||
|
||||
// Horizontal row layout for arranging buttons.
|
||||
var rowLayout = LinearLayout(activity)
|
||||
rowLayout.orientation = LinearLayout.HORIZONTAL
|
||||
rowLayout.layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
|
||||
// Calculate the maximum width for a button to allow two buttons per row.
|
||||
val screenWidth = activity.resources.displayMetrics.widthPixels
|
||||
val availableWidth = screenWidth - (2 * paddingHorizontal)
|
||||
val maxButtonWidth = availableWidth / 2
|
||||
|
||||
buttons.forEachIndexed { index, buttonLabel ->
|
||||
val button = Button(activity)
|
||||
button.text = buttonLabel
|
||||
button.isSingleLine = true
|
||||
button.setPadding(buttonPadding, buttonPadding, buttonPadding, buttonPadding)
|
||||
|
||||
// Measure the button to determine its width.
|
||||
button.measure(0, 0)
|
||||
val buttonWidth = button.measuredWidth
|
||||
|
||||
val params = LinearLayout.LayoutParams(
|
||||
if (buttonWidth > maxButtonWidth) LinearLayout.LayoutParams.MATCH_PARENT else 0,
|
||||
buttonHeight
|
||||
)
|
||||
params.weight = if (buttonWidth > maxButtonWidth) 0f else 1f
|
||||
button.layoutParams = params
|
||||
|
||||
// Handle full-width buttons by finalizing the current row, if needed.
|
||||
if (buttonWidth > maxButtonWidth) {
|
||||
if (rowLayout.childCount > 0) {
|
||||
parentLayout.addView(rowLayout)
|
||||
rowLayout = LinearLayout(activity)
|
||||
rowLayout.orientation = LinearLayout.HORIZONTAL
|
||||
}
|
||||
// Add the full-width button directly to the parent layout.
|
||||
parentLayout.addView(button)
|
||||
} else {
|
||||
rowLayout.addView(button)
|
||||
|
||||
// Finalize the row if it reaches 2 buttons.
|
||||
if (rowLayout.childCount == 2) {
|
||||
parentLayout.addView(rowLayout)
|
||||
rowLayout = LinearLayout(activity)
|
||||
rowLayout.orientation = LinearLayout.HORIZONTAL
|
||||
}
|
||||
|
||||
// Handle the last button with incomplete row.
|
||||
if (index == buttons.size - 1 && rowLayout.childCount > 0) {
|
||||
parentLayout.addView(rowLayout)
|
||||
}
|
||||
}
|
||||
|
||||
button.setOnClickListener {
|
||||
dialogCallback(index)
|
||||
dismissDialog()
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the parent layout to the dialog.
|
||||
builder.setView(parentLayout)
|
||||
val dialog = builder.create()
|
||||
dismissDialog = {dialog.dismiss()}
|
||||
dialog.setCancelable(false)
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method shows a dialog with a text input field, allowing the user to input text.
|
||||
*
|
||||
* @param activity The activity where the input dialog will be displayed.
|
||||
* @param title The title of the input dialog.
|
||||
* @param message The message displayed in the input dialog.
|
||||
* @param existingText The existing text that will be pre-filled in the input field.
|
||||
*/
|
||||
internal fun showInputDialog(activity: Activity, title: String, message: String, existingText: String) {
|
||||
val inputField = EditText(activity)
|
||||
val paddingHorizontal = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_horizontal)
|
||||
val paddingVertical = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_vertical)
|
||||
inputField.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
||||
inputField.setText(existingText)
|
||||
activity.runOnUiThread {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(message).setTitle(title).setView(inputField)
|
||||
builder.setPositiveButton(R.string.dialog_ok) {
|
||||
dialog: DialogInterface, id: Int ->
|
||||
inputDialogCallback(inputField.text.toString())
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.setCancelable(false)
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a Snackbar with an optional action button.
|
||||
*
|
||||
* @param context The Context in which the Snackbar should be displayed.
|
||||
* @param message The message to display in the Snackbar.
|
||||
* @param duration The duration for which the Snackbar should be visible (in milliseconds).
|
||||
* @param actionText (Optional) The text for the action button. If `null`, the button is hidden.
|
||||
* @param actionCallback (Optional) A callback function to execute when the action button is clicked. If `null`, no action is performed.
|
||||
*/
|
||||
fun showSnackbar(activity: Activity, message: String, duration: Long = 3000, actionText: String? = null, action: (() -> Unit)? = null) {
|
||||
activity.runOnUiThread {
|
||||
val bottomMargin = activity.resources.getDimensionPixelSize(R.dimen.snackbar_bottom_margin)
|
||||
val inflater = LayoutInflater.from(activity)
|
||||
val customView = inflater.inflate(R.layout.snackbar, null)
|
||||
|
||||
val popupWindow = PopupWindow(
|
||||
customView,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
|
||||
val messageView = customView.findViewById<TextView>(R.id.snackbar_text)
|
||||
messageView.text = message
|
||||
|
||||
val actionButton = customView.findViewById<Button>(R.id.snackbar_action)
|
||||
|
||||
if (actionText != null && action != null) {
|
||||
actionButton.text = actionText
|
||||
actionButton.visibility = View.VISIBLE
|
||||
actionButton.setOnClickListener {
|
||||
action.invoke()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
} else {
|
||||
actionButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
addSwipeToDismiss(customView, popupWindow)
|
||||
popupWindow.showAtLocation(customView, Gravity.BOTTOM, 0, bottomMargin)
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({ popupWindow.dismiss() }, duration)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSwipeToDismiss(view: View, popupWindow: PopupWindow) {
|
||||
view.setOnTouchListener(object : View.OnTouchListener {
|
||||
private var initialX = 0f
|
||||
private var dX = 0f
|
||||
private val threshold = 300f // Swipe distance threshold.
|
||||
|
||||
override fun onTouch(v: View?, event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
initialX = event.rawX
|
||||
dX = view.translationX
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val deltaX = event.rawX - initialX
|
||||
view.translationX = dX + deltaX
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
val finalX = event.rawX - initialX
|
||||
|
||||
if (abs(finalX) > threshold) {
|
||||
// If swipe exceeds threshold, dismiss smoothly.
|
||||
view.animate()
|
||||
.translationX(if (finalX > 0) view.width.toFloat() else -view.width.toFloat())
|
||||
.setDuration(200)
|
||||
.withEndAction { popupWindow.dismiss() }
|
||||
.start()
|
||||
} else {
|
||||
// If swipe is canceled, return smoothly.
|
||||
view.animate()
|
||||
.translationX(0f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/**************************************************************************/
|
||||
/* GameMenuUtils.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.utils
|
||||
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.GodotLib
|
||||
|
||||
/**
|
||||
* Utility class for accessing and using game menu APIs.
|
||||
*/
|
||||
object GameMenuUtils {
|
||||
private val TAG = GameMenuUtils::class.java.simpleName
|
||||
|
||||
/**
|
||||
* Enum representing the "run/window_placement/game_embed_mode" editor settings.
|
||||
*/
|
||||
enum class GameEmbedMode(internal val nativeValue: Int) {
|
||||
DISABLED(-1), AUTO(0), ENABLED(1);
|
||||
|
||||
companion object {
|
||||
internal const val SETTING_KEY = "run/window_placement/game_embed_mode"
|
||||
|
||||
@JvmStatic
|
||||
internal fun fromNativeValue(nativeValue: Int): GameEmbedMode? {
|
||||
for (mode in GameEmbedMode.entries) {
|
||||
if (mode.nativeValue == nativeValue) {
|
||||
return mode
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
external fun setSuspend(enabled: Boolean)
|
||||
|
||||
@JvmStatic
|
||||
external fun nextFrame()
|
||||
|
||||
@JvmStatic
|
||||
external fun setNodeType(type: Int)
|
||||
|
||||
@JvmStatic
|
||||
external fun setSelectMode(mode: Int)
|
||||
|
||||
@JvmStatic
|
||||
external fun setSelectionVisible(visible: Boolean)
|
||||
|
||||
@JvmStatic
|
||||
external fun setCameraOverride(enabled: Boolean)
|
||||
|
||||
@JvmStatic
|
||||
external fun setCameraManipulateMode(mode: Int)
|
||||
|
||||
@JvmStatic
|
||||
external fun resetCamera2DPosition()
|
||||
|
||||
@JvmStatic
|
||||
external fun resetCamera3DPosition()
|
||||
|
||||
@JvmStatic
|
||||
external fun playMainScene()
|
||||
|
||||
/**
|
||||
* Returns [GameEmbedMode] stored in the editor settings.
|
||||
*
|
||||
* Must be called on the render thread.
|
||||
*/
|
||||
fun fetchGameEmbedMode(): GameEmbedMode {
|
||||
try {
|
||||
val gameEmbedModeValue = Integer.parseInt(GodotLib.getEditorSetting(GameEmbedMode.SETTING_KEY))
|
||||
val gameEmbedMode = GameEmbedMode.fromNativeValue(gameEmbedModeValue) ?: GameEmbedMode.AUTO
|
||||
return gameEmbedMode
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to retrieve game embed mode", e)
|
||||
return GameEmbedMode.AUTO
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the 'game_embed_mode' editor setting.
|
||||
*
|
||||
* Must be called on the render thread.
|
||||
*/
|
||||
fun saveGameEmbedMode(gameEmbedMode: GameEmbedMode) {
|
||||
GodotLib.setEditorSetting(GameEmbedMode.SETTING_KEY, gameEmbedMode.nativeValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ import android.provider.Settings;
|
|||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ public final class PermissionsUtil {
|
|||
public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
|
||||
public static final int REQUEST_SINGLE_PERMISSION_REQ_CODE = 1002;
|
||||
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002;
|
||||
public static final int REQUEST_INSTALL_PACKAGES_REQ_CODE = 3002;
|
||||
|
||||
private PermissionsUtil() {
|
||||
}
|
||||
|
|
@ -76,11 +78,11 @@ public final class PermissionsUtil {
|
|||
* @param activity the caller activity for this method.
|
||||
* @return true/false. "true" if permissions are already granted, "false" if a permissions request was dispatched.
|
||||
*/
|
||||
public static boolean requestPermissions(Activity activity, List<String> permissions) {
|
||||
if (activity == null) {
|
||||
return false;
|
||||
}
|
||||
public static boolean requestPermissions(@NonNull Activity activity, List<String> permissions) {
|
||||
return requestPermissions(activity, permissions, REQUEST_ALL_PERMISSION_REQ_CODE);
|
||||
}
|
||||
|
||||
private static boolean requestPermissions(@NonNull Activity activity, List<String> permissions, int requestCode) {
|
||||
if (permissions == null || permissions.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -90,6 +92,7 @@ public final class PermissionsUtil {
|
|||
return true;
|
||||
}
|
||||
|
||||
boolean dispatchedPermissionsRequest = false;
|
||||
Set<String> requestedPermissions = new HashSet<>();
|
||||
for (String permission : permissions) {
|
||||
try {
|
||||
|
|
@ -104,11 +107,23 @@ public final class PermissionsUtil {
|
|||
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
||||
activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
|
||||
}
|
||||
dispatchedPermissionsRequest = true;
|
||||
}
|
||||
} else if (permission.equals(Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !activity.getPackageManager().canRequestPackageInstalls()) {
|
||||
try {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
|
||||
intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
|
||||
activity.startActivityForResult(intent, REQUEST_INSTALL_PACKAGES_REQ_CODE);
|
||||
dispatchedPermissionsRequest = true;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Unable to request permission " + Manifest.permission.REQUEST_INSTALL_PACKAGES);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PermissionInfo permissionInfo = getPermissionInfo(activity, permission);
|
||||
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
|
||||
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
if ((protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Requesting permission " + permission);
|
||||
requestedPermissions.add(permission);
|
||||
}
|
||||
|
|
@ -119,13 +134,12 @@ public final class PermissionsUtil {
|
|||
}
|
||||
}
|
||||
|
||||
if (requestedPermissions.isEmpty()) {
|
||||
// If list is empty, all of dangerous permissions were granted.
|
||||
return true;
|
||||
if (!requestedPermissions.isEmpty()) {
|
||||
activity.requestPermissions(requestedPermissions.toArray(new String[0]), requestCode);
|
||||
dispatchedPermissionsRequest = true;
|
||||
}
|
||||
|
||||
activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE);
|
||||
return false;
|
||||
return !dispatchedPermissionsRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -134,57 +148,37 @@ public final class PermissionsUtil {
|
|||
* @param activity the caller activity for this method.
|
||||
* @return true/false. "true" if permission is already granted, "false" if a permission request was dispatched.
|
||||
*/
|
||||
public static boolean requestPermission(String permissionName, Activity activity) {
|
||||
if (activity == null || TextUtils.isEmpty(permissionName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
// Not necessary, asked on install already
|
||||
public static boolean requestPermission(String permissionName, @NonNull Activity activity) {
|
||||
if (TextUtils.isEmpty(permissionName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final int requestCode;
|
||||
final String updatedPermissionName;
|
||||
switch (permissionName) {
|
||||
case "RECORD_AUDIO":
|
||||
case Manifest.permission.RECORD_AUDIO:
|
||||
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
activity.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO_PERMISSION);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
updatedPermissionName = Manifest.permission.RECORD_AUDIO;
|
||||
requestCode = REQUEST_RECORD_AUDIO_PERMISSION;
|
||||
break;
|
||||
|
||||
case "CAMERA":
|
||||
case Manifest.permission.CAMERA:
|
||||
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
||||
activity.requestPermissions(new String[] { Manifest.permission.CAMERA }, REQUEST_CAMERA_PERMISSION);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
updatedPermissionName = Manifest.permission.CAMERA;
|
||||
requestCode = REQUEST_CAMERA_PERMISSION;
|
||||
break;
|
||||
|
||||
case "VIBRATE":
|
||||
case Manifest.permission.VIBRATE:
|
||||
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) {
|
||||
activity.requestPermissions(new String[] { Manifest.permission.VIBRATE }, REQUEST_VIBRATE_PERMISSION);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
updatedPermissionName = Manifest.permission.VIBRATE;
|
||||
requestCode = REQUEST_VIBRATE_PERMISSION;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Check if the given permission is a dangerous permission
|
||||
try {
|
||||
PermissionInfo permissionInfo = getPermissionInfo(activity, permissionName);
|
||||
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
|
||||
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permissionName) != PackageManager.PERMISSION_GRANTED) {
|
||||
activity.requestPermissions(new String[] { permissionName }, REQUEST_SINGLE_PERMISSION_REQ_CODE);
|
||||
return false;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
// Unknown permission - return false as it can't be granted.
|
||||
Log.w(TAG, "Unable to identify permission " + permissionName, e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
updatedPermissionName = permissionName;
|
||||
requestCode = REQUEST_SINGLE_PERMISSION_REQ_CODE;
|
||||
break;
|
||||
}
|
||||
|
||||
List<String> permissions = Collections.singletonList(updatedPermissionName);
|
||||
return requestPermissions(activity, permissions, requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -215,7 +209,7 @@ public final class PermissionsUtil {
|
|||
try {
|
||||
manifestPermissions = getManifestPermissions(activity);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to retrieve manifest permissions", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +236,7 @@ public final class PermissionsUtil {
|
|||
try {
|
||||
manifestPermissions = getManifestPermissions(context);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to retrieve manifest permissions", e);
|
||||
return new String[0];
|
||||
}
|
||||
if (manifestPermissions.isEmpty()) {
|
||||
|
|
@ -259,7 +253,7 @@ public final class PermissionsUtil {
|
|||
} else {
|
||||
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(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
|
||||
if ((protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
|
||||
grantedPermissions.add(manifestPermission);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ package org.godotengine.godot.utils;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityOptions;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
|
@ -44,6 +45,9 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
|||
*/
|
||||
public final class ProcessPhoenix extends Activity {
|
||||
private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents";
|
||||
// -- GODOT start --
|
||||
private static final String KEY_RESTART_ACTIVITY_OPTIONS = "phoenix_restart_activity_options";
|
||||
// -- GODOT end --
|
||||
private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid";
|
||||
|
||||
/**
|
||||
|
|
@ -56,12 +60,23 @@ public final class ProcessPhoenix extends Activity {
|
|||
triggerRebirth(context, getRestartIntent(context));
|
||||
}
|
||||
|
||||
// -- GODOT start --
|
||||
/**
|
||||
* Call to restart the application process using the specified intents.
|
||||
* <p>
|
||||
* Behavior of the current process after invoking this method is undefined.
|
||||
*/
|
||||
public static void triggerRebirth(Context context, Intent... nextIntents) {
|
||||
triggerRebirth(context, null, nextIntents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to restart the application process using the specified intents launched with the given
|
||||
* {@link ActivityOptions}.
|
||||
* <p>
|
||||
* Behavior of the current process after invoking this method is undefined.
|
||||
*/
|
||||
public static void triggerRebirth(Context context, Bundle activityOptions, Intent... nextIntents) {
|
||||
if (nextIntents.length < 1) {
|
||||
throw new IllegalArgumentException("intents cannot be empty");
|
||||
}
|
||||
|
|
@ -72,10 +87,12 @@ public final class ProcessPhoenix extends Activity {
|
|||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
|
||||
intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
|
||||
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
|
||||
if (activityOptions != null) {
|
||||
intent.putExtra(KEY_RESTART_ACTIVITY_OPTIONS, activityOptions);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
// -- GODOT start --
|
||||
/**
|
||||
* Finish the activity and kill its process
|
||||
*/
|
||||
|
|
@ -112,9 +129,11 @@ public final class ProcessPhoenix extends Activity {
|
|||
super.onCreate(savedInstanceState);
|
||||
|
||||
// -- GODOT start --
|
||||
ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
|
||||
startActivities(intents.toArray(new Intent[intents.size()]));
|
||||
forceQuit(this, getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1));
|
||||
Intent launchIntent = getIntent();
|
||||
ArrayList<Intent> intents = launchIntent.getParcelableArrayListExtra(KEY_RESTART_INTENTS);
|
||||
Bundle activityOptions = launchIntent.getBundleExtra(KEY_RESTART_ACTIVITY_OPTIONS);
|
||||
startActivities(intents.toArray(new Intent[intents.size()]), activityOptions);
|
||||
forceQuit(this, launchIntent.getIntExtra(KEY_MAIN_PROCESS_PID, -1));
|
||||
// -- GODOT end --
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
/**************************************************************************/
|
||||
/* Callable.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.variant
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
/**
|
||||
* Android version of a Godot built-in Callable type representing a method or a standalone function.
|
||||
*/
|
||||
@Keep
|
||||
class Callable private constructor(private val nativeCallablePointer: Long) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Invoke method [methodName] on the Godot object specified by [godotObjectId]
|
||||
*/
|
||||
@JvmStatic
|
||||
fun call(godotObjectId: Long, methodName: String, vararg methodParameters: Any): Any? {
|
||||
return nativeCallObject(godotObjectId, methodName, methodParameters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke method [methodName] on the Godot object specified by [godotObjectId] during idle time.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun callDeferred(godotObjectId: Long, methodName: String, vararg methodParameters: Any) {
|
||||
nativeCallObjectDeferred(godotObjectId, methodName, methodParameters)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeCall(pointer: Long, params: Array<out Any>): Any?
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeCallObject(godotObjectId: Long, methodName: String, params: Array<out Any>): Any?
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeCallObjectDeferred(godotObjectId: Long, methodName: String, params: Array<out Any>)
|
||||
|
||||
@JvmStatic
|
||||
private external fun releaseNativePointer(nativePointer: Long)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the method represented by this [Callable]. Arguments can be passed and should match the method's signature.
|
||||
*/
|
||||
internal fun call(vararg params: Any): Any? {
|
||||
if (nativeCallablePointer == 0L) {
|
||||
return null
|
||||
}
|
||||
|
||||
return nativeCall(nativeCallablePointer, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to provide access to the native callable pointer to the native logic.
|
||||
*/
|
||||
private fun getNativePointer() = nativeCallablePointer
|
||||
|
||||
/** Note that [finalize] is deprecated and shouldn't be used, unfortunately its replacement,
|
||||
* [java.lang.ref.Cleaner], is only available on Android api 33 and higher.
|
||||
* So we resort to using it for the time being until our min api catches up to api 33.
|
||||
**/
|
||||
protected fun finalize() {
|
||||
releaseNativePointer(nativeCallablePointer)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue