summaryrefslogtreecommitdiffstats
path: root/platform/android
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android')
-rw-r--r--platform/android/api/java_class_wrapper.h3
-rw-r--r--platform/android/api/jni_singleton.h11
-rw-r--r--platform/android/dir_access_jandroid.cpp8
-rw-r--r--platform/android/dir_access_jandroid.h1
-rw-r--r--platform/android/display_server_android.cpp15
-rw-r--r--platform/android/display_server_android.h1
-rw-r--r--platform/android/doc_classes/EditorExportPlatformAndroid.xml60
-rw-r--r--platform/android/export/export.cpp7
-rw-r--r--platform/android/export/export_plugin.cpp156
-rw-r--r--platform/android/export/export_plugin.h12
-rw-r--r--platform/android/file_access_android.cpp17
-rw-r--r--platform/android/file_access_android.h11
-rw-r--r--platform/android/file_access_filesystem_jandroid.cpp36
-rw-r--r--platform/android/file_access_filesystem_jandroid.h3
-rw-r--r--platform/android/java/app/build.gradle30
-rw-r--r--platform/android/java/app/config.gradle8
-rw-r--r--platform/android/java/editor/build.gradle2
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt2
-rw-r--r--platform/android/java/lib/build.gradle5
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.kt171
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt31
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java19
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt17
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt39
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt53
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt83
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java134
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt6
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java10
-rw-r--r--platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt104
-rw-r--r--platform/android/java_class_wrapper.cpp18
-rw-r--r--platform/android/java_godot_io_wrapper.cpp11
-rw-r--r--platform/android/java_godot_lib_jni.cpp14
-rw-r--r--platform/android/java_godot_view_wrapper.cpp1
-rw-r--r--platform/android/java_godot_wrapper.cpp20
-rw-r--r--platform/android/net_socket_android.cpp8
-rw-r--r--platform/android/net_socket_android.h1
-rw-r--r--platform/android/os_android.cpp76
-rw-r--r--platform/android/os_android.h4
-rw-r--r--platform/android/tts_android.cpp10
-rw-r--r--platform/android/tts_android.h1
41 files changed, 941 insertions, 278 deletions
diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h
index b1481ebf7b..e21a331ab9 100644
--- a/platform/android/api/java_class_wrapper.h
+++ b/platform/android/api/java_class_wrapper.h
@@ -209,8 +209,6 @@ class JavaClassWrapper : public Object {
#ifdef ANDROID_ENABLED
RBMap<String, Ref<JavaClass>> class_cache;
friend class JavaClass;
- jclass activityClass;
- jmethodID findClass;
jmethodID getDeclaredMethods;
jmethodID getFields;
jmethodID getParameterTypes;
@@ -229,7 +227,6 @@ class JavaClassWrapper : public Object {
jmethodID Long_longValue;
jmethodID Float_floatValue;
jmethodID Double_doubleValue;
- jobject classLoader;
bool _get_type_sig(JNIEnv *env, jobject obj, uint32_t &sig, String &strsig);
#endif
diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h
index a2d1c08168..5b30c392e7 100644
--- a/platform/android/api/jni_singleton.h
+++ b/platform/android/api/jni_singleton.h
@@ -241,6 +241,17 @@ public:
instance = nullptr;
#endif
}
+
+ ~JNISingleton() {
+#ifdef ANDROID_ENABLED
+ if (instance) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(instance);
+ }
+#endif
+ }
};
#endif // JNI_SINGLETON_H
diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp
index 972a7dbe6a..ab90527bfa 100644
--- a/platform/android/dir_access_jandroid.cpp
+++ b/platform/android/dir_access_jandroid.cpp
@@ -321,6 +321,14 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) {
_current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(II)Z");
}
+void DirAccessJAndroid::terminate() {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(cls);
+ env->DeleteGlobalRef(dir_access_handler);
+}
+
DirAccessJAndroid::DirAccessJAndroid() {
}
diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h
index 9aaa78f38c..68578b0fa9 100644
--- a/platform/android/dir_access_jandroid.h
+++ b/platform/android/dir_access_jandroid.h
@@ -89,6 +89,7 @@ public:
virtual uint64_t get_space_left() override;
static void setup(jobject p_dir_access_handler);
+ static void terminate();
DirAccessJAndroid();
~DirAccessJAndroid();
diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp
index 01ecbc7164..c6f2f82117 100644
--- a/platform/android/display_server_android.cpp
+++ b/platform/android/display_server_android.cpp
@@ -58,15 +58,21 @@ DisplayServerAndroid *DisplayServerAndroid::get_singleton() {
bool DisplayServerAndroid::has_feature(Feature p_feature) const {
switch (p_feature) {
+#ifndef DISABLE_DEPRECATED
+ case FEATURE_GLOBAL_MENU: {
+ return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU));
+ } break;
+#endif
case FEATURE_CURSOR_SHAPE:
//case FEATURE_CUSTOM_CURSOR_SHAPE:
- //case FEATURE_GLOBAL_MENU:
//case FEATURE_HIDPI:
//case FEATURE_ICON:
//case FEATURE_IME:
case FEATURE_MOUSE:
//case FEATURE_MOUSE_WARP:
//case FEATURE_NATIVE_DIALOG:
+ //case FEATURE_NATIVE_DIALOG_INPUT:
+ //case FEATURE_NATIVE_DIALOG_FILE:
//case FEATURE_NATIVE_ICON:
//case FEATURE_WINDOW_TRANSPARENCY:
case FEATURE_CLIPBOARD:
@@ -578,6 +584,8 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
keep_screen_on = GLOBAL_GET("display/window/energy_saving/keep_screen_on");
+ native_menu = memnew(NativeMenu);
+
#if defined(GLES3_ENABLED)
if (rendering_driver == "opengl3") {
RasterizerGLES3::make_current(false);
@@ -641,6 +649,11 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
}
DisplayServerAndroid::~DisplayServerAndroid() {
+ if (native_menu) {
+ memdelete(native_menu);
+ native_menu = nullptr;
+ }
+
#if defined(RD_ENABLED)
if (rendering_device) {
memdelete(rendering_device);
diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h
index c95eaddf93..e1914f4d18 100644
--- a/platform/android/display_server_android.h
+++ b/platform/android/display_server_android.h
@@ -76,6 +76,7 @@ class DisplayServerAndroid : public DisplayServer {
RenderingContextDriver *rendering_context = nullptr;
RenderingDevice *rendering_device = nullptr;
#endif
+ NativeMenu *native_menu = nullptr;
ObjectID window_attached_instance_id;
diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml
index 64485afeb0..020e432155 100644
--- a/platform/android/doc_classes/EditorExportPlatformAndroid.xml
+++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml
@@ -15,11 +15,11 @@
Array of random bytes that the licensing Policy uses to create an [url=https://developer.android.com/google/play/licensing/adding-licensing#impl-Obfuscator]Obfuscator[/url].
</member>
<member name="apk_expansion/enable" type="bool" setter="" getter="">
- If [code]true[/code], project resources are stored in the separate APK expansion file, instead APK.
- [b]Note:[/b] APK expansion should be enabled to use PCK encryption.
+ If [code]true[/code], project resources are stored in the separate APK expansion file, instead of the APK.
+ [b]Note:[/b] APK expansion should be enabled to use PCK encryption. See [url=https://developer.android.com/google/play/expansion-files]APK Expansion Files[/url]
</member>
<member name="apk_expansion/public_key" type="String" setter="" getter="">
- Base64 encoded RSA public key for your publisher account, available from the profile page on the "Play Console".
+ Base64 encoded RSA public key for your publisher account, available from the profile page on the "Google Play Console".
</member>
<member name="architectures/arm64-v8a" type="bool" setter="" getter="">
If [code]true[/code], [code]arm64[/code] binaries are included into exported project.
@@ -34,7 +34,7 @@
If [code]true[/code], [code]x86_64[/code] binaries are included into exported project.
</member>
<member name="command_line/extra_args" type="String" setter="" getter="">
- A list of additional command line arguments, exported project will receive when started.
+ A list of additional command line arguments, separated by space, which the exported project will receive when started.
</member>
<member name="custom_template/debug" type="String" setter="" getter="">
Path to an APK file to use as a custom export template for debug exports. If left empty, default template is used.
@@ -52,16 +52,16 @@
[b]Note:[/b] Although your binary may be smaller, your application may load slower because the native libraries are not loaded directly from the binary at runtime.
</member>
<member name="gradle_build/export_format" type="int" setter="" getter="">
- Export format for Gradle build.
+ Application export format (*.apk or *.aab).
</member>
<member name="gradle_build/gradle_build_directory" type="String" setter="" getter="">
Path to the Gradle build directory. If left empty, then [code]res://android[/code] will be used.
</member>
<member name="gradle_build/min_sdk" type="String" setter="" getter="">
- Minimal Android SDK version for Gradle build.
+ Minimum Android API level required for the application to run (used during Gradle build). See [url=https://developer.android.com/guide/topics/manifest/uses-sdk-element#uses]android:minSdkVersion[/url].
</member>
<member name="gradle_build/target_sdk" type="String" setter="" getter="">
- Target Android SDK version for Gradle build.
+ The Android API level on which the application is designed to run (used during Gradle build). See [url=https://developer.android.com/guide/topics/manifest/uses-sdk-element#uses]android:targetSdkVersion[/url].
</member>
<member name="gradle_build/use_gradle_build" type="bool" setter="" getter="">
If [code]true[/code], Gradle build is used instead of pre-built APK.
@@ -97,25 +97,25 @@
Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_RELEASE_USER[/code].
</member>
<member name="launcher_icons/adaptive_background_432x432" type="String" setter="" getter="">
- Background layer of the application adaptive icon file.
+ Background layer of the application adaptive icon file. See [url=https://developer.android.com/develop/ui/views/launch/icon_design_adaptive#design-adaptive-icons]Design adaptive icons[/url].
</member>
<member name="launcher_icons/adaptive_foreground_432x432" type="String" setter="" getter="">
- Foreground layer of the application adaptive icon file.
+ Foreground layer of the application adaptive icon file. See [url=https://developer.android.com/develop/ui/views/launch/icon_design_adaptive#design-adaptive-icons]Design adaptive icons[/url].
</member>
<member name="launcher_icons/main_192x192" type="String" setter="" getter="">
Application icon file. If left empty, it will fallback to [member ProjectSettings.application/config/icon].
</member>
<member name="package/app_category" type="int" setter="" getter="">
- Application category for the Play Store.
+ Application category for the Google Play Store. Only define this if your application fits one of the categories well. See [url=https://developer.android.com/guide/topics/manifest/application-element#appCategory]android:appCategory[/url].
</member>
<member name="package/exclude_from_recents" type="bool" setter="" getter="">
- If [code]true[/code], task initiated by main activity will be excluded from the list of recently used applications.
+ If [code]true[/code], task initiated by main activity will be excluded from the list of recently used applications. See [url=https://developer.android.com/guide/topics/manifest/activity-element#exclude]android:excludeFromRecents[/url].
</member>
<member name="package/name" type="String" setter="" getter="">
Name of the application.
</member>
<member name="package/retain_data_on_uninstall" type="bool" setter="" getter="">
- If [code]true[/code], when the user uninstalls an app, a prompt to keep the app's data will be shown.
+ If [code]true[/code], when the user uninstalls an app, a prompt to keep the app's data will be shown. See [url=https://developer.android.com/guide/topics/manifest/application-element#fragileuserdata]android:hasFragileUserData[/url].
</member>
<member name="package/show_as_launcher_app" type="bool" setter="" getter="">
If [code]true[/code], the user will be able to set this app as the system launcher in Android preferences.
@@ -274,8 +274,7 @@
<member name="permissions/custom_permissions" type="PackedStringArray" setter="" getter="">
Array of custom permission strings.
</member>
- <member name="permissions/delete_cache_files" type="bool" setter="" getter="">
- Deprecated.
+ <member name="permissions/delete_cache_files" type="bool" setter="" getter="" deprecated="">
</member>
<member name="permissions/delete_packages" type="bool" setter="" getter="">
Allows an application to delete packages. See [url=https://developer.android.com/reference/android/Manifest.permission#DELETE_PACKAGES]DELETE_PACKAGES[/url].
@@ -310,8 +309,7 @@
<member name="permissions/get_package_size" type="bool" setter="" getter="">
Allows an application to find out the space used by any package. See [url=https://developer.android.com/reference/android/Manifest.permission#GET_PACKAGE_SIZE]GET_PACKAGE_SIZE[/url].
</member>
- <member name="permissions/get_tasks" type="bool" setter="" getter="">
- Deprecated in API level 21.
+ <member name="permissions/get_tasks" type="bool" setter="" getter="" deprecated="Deprecated in API level 21.">
</member>
<member name="permissions/get_top_activity_info" type="bool" setter="" getter="">
Allows an application to retrieve private information about the current top activity.
@@ -379,13 +377,14 @@
<member name="permissions/nfc" type="bool" setter="" getter="">
Allows applications to perform I/O operations over NFC. See [url=https://developer.android.com/reference/android/Manifest.permission#NFC]NFC[/url].
</member>
- <member name="permissions/persistent_activity" type="bool" setter="" getter="">
- Allow an application to make its activities persistent.
- Deprecated in API level 15.
+ <member name="permissions/persistent_activity" type="bool" setter="" getter="" deprecated="Deprecated in API level 15.">
+ Allows an application to make its activities persistent.
</member>
- <member name="permissions/process_outgoing_calls" type="bool" setter="" getter="">
+ <member name="permissions/post_notifications" type="bool" setter="" getter="">
+ Allows an application to post notifications. Added in API level 33. See [url=https://developer.android.com/develop/ui/views/notifications/notification-permission]Notification runtime permission[/url].
+ </member>
+ <member name="permissions/process_outgoing_calls" type="bool" setter="" getter="" deprecated="Deprecated in API level 29.">
Allows an application to see the number being dialed during an outgoing call with the option to redirect the call to a different number or abort the call altogether. See [url=https://developer.android.com/reference/android/Manifest.permission#PROCESS_OUTGOING_CALLS]PROCESS_OUTGOING_CALLS[/url].
- Deprecated in API level 29.
</member>
<member name="permissions/read_calendar" type="bool" setter="" getter="">
Allows an application to read the user's calendar data. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CALENDAR]READ_CALENDAR[/url].
@@ -396,9 +395,8 @@
<member name="permissions/read_contacts" type="bool" setter="" getter="">
Allows an application to read the user's contacts data. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CONTACTS]READ_CONTACTS[/url].
</member>
- <member name="permissions/read_external_storage" type="bool" setter="" getter="">
+ <member name="permissions/read_external_storage" type="bool" setter="" getter="" deprecated="Deprecated in API level 33.">
Allows an application to read from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_EXTERNAL_STORAGE]READ_EXTERNAL_STORAGE[/url].
- Deprecated in API level 33.
</member>
<member name="permissions/read_frame_buffer" type="bool" setter="" getter="">
Allows an application to take screen shots and more generally get access to the frame buffer data.
@@ -406,8 +404,7 @@
<member name="permissions/read_history_bookmarks" type="bool" setter="" getter="">
Allows an application to read (but not write) the user's browsing history and bookmarks.
</member>
- <member name="permissions/read_input_state" type="bool" setter="" getter="">
- Deprecated in API level 16.
+ <member name="permissions/read_input_state" type="bool" setter="" getter="" deprecated="Deprecated in API level 16.">
</member>
<member name="permissions/read_logs" type="bool" setter="" getter="">
Allows an application to read the low-level system log files. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_LOGS]READ_LOGS[/url].
@@ -454,8 +451,7 @@
<member name="permissions/reorder_tasks" type="bool" setter="" getter="">
Allows an application to change the Z-order of tasks. See [url=https://developer.android.com/reference/android/Manifest.permission#REORDER_TASKS]REORDER_TASKS[/url].
</member>
- <member name="permissions/restart_packages" type="bool" setter="" getter="">
- Deprecated in API level 15.
+ <member name="permissions/restart_packages" type="bool" setter="" getter="" deprecated="Deprecated in API level 15.">
</member>
<member name="permissions/send_respond_via_message" type="bool" setter="" getter="">
Allows an application (Phone) to send a request to other applications to handle the respond-via-message action during incoming calls. See [url=https://developer.android.com/reference/android/Manifest.permission#SEND_RESPOND_VIA_MESSAGE]SEND_RESPOND_VIA_MESSAGE[/url].
@@ -484,8 +480,7 @@
<member name="permissions/set_pointer_speed" type="bool" setter="" getter="">
Allows low-level access to setting the pointer speed.
</member>
- <member name="permissions/set_preferred_applications" type="bool" setter="" getter="">
- Deprecated in API level 15.
+ <member name="permissions/set_preferred_applications" type="bool" setter="" getter="" deprecated="Deprecated in API level 15.">
</member>
<member name="permissions/set_process_limit" type="bool" setter="" getter="">
Allows an application to set the maximum number of (not needed) application processes that can be running. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_PROCESS_LIMIT]SET_PROCESS_LIMIT[/url].
@@ -511,8 +506,7 @@
<member name="permissions/subscribed_feeds_read" type="bool" setter="" getter="">
Allows an application to allow access the subscribed feeds ContentProvider.
</member>
- <member name="permissions/subscribed_feeds_write" type="bool" setter="" getter="">
- Deprecated.
+ <member name="permissions/subscribed_feeds_write" type="bool" setter="" getter="" deprecated="">
</member>
<member name="permissions/system_alert_window" type="bool" setter="" getter="">
Allows an app to create windows using the type WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, shown on top of all other apps. See [url=https://developer.android.com/reference/android/Manifest.permission#SYSTEM_ALERT_WINDOW]SYSTEM_ALERT_WINDOW[/url].
@@ -520,8 +514,7 @@
<member name="permissions/transmit_ir" type="bool" setter="" getter="">
Allows using the device's IR transmitter, if available. See [url=https://developer.android.com/reference/android/Manifest.permission#TRANSMIT_IR]TRANSMIT_IR[/url].
</member>
- <member name="permissions/uninstall_shortcut" type="bool" setter="" getter="">
- Deprecated.
+ <member name="permissions/uninstall_shortcut" type="bool" setter="" getter="" deprecated="">
</member>
<member name="permissions/update_device_stats" type="bool" setter="" getter="">
Allows an application to update device statistics. See [url=https://developer.android.com/reference/android/Manifest.permission#UPDATE_DEVICE_STATS]UPDATE_DEVICE_STATS[/url].
@@ -605,6 +598,7 @@
Application version visible to the user. Falls back to [member ProjectSettings.application/config/version] if left empty.
</member>
<member name="xr_features/xr_mode" type="int" setter="" getter="">
+ The extended reality (XR) mode for this application.
</member>
</members>
</class>
diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp
index 138714634f..6a6d7149ff 100644
--- a/platform/android/export/export.cpp
+++ b/platform/android/export/export.cpp
@@ -33,6 +33,7 @@
#include "export_plugin.h"
#include "core/os/os.h"
+#include "editor/editor_paths.h"
#include "editor/editor_settings.h"
#include "editor/export/editor_export.h"
@@ -46,10 +47,10 @@ void register_android_exporter() {
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
- EDITOR_DEF("export/android/debug_keystore", "");
+ EDITOR_DEF("export/android/debug_keystore", EditorPaths::get_singleton()->get_debug_keystore_path());
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"));
- EDITOR_DEF("export/android/debug_keystore_user", "androiddebugkey");
- EDITOR_DEF("export/android/debug_keystore_pass", "android");
+ EDITOR_DEF("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
+ EDITOR_DEF("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore_pass", PROPERTY_HINT_PASSWORD));
EDITOR_DEF("export/android/force_system_user", false);
diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp
index a485b57a64..3b1a534daf 100644
--- a/platform/android/export/export_plugin.cpp
+++ b/platform/android/export/export_plugin.cpp
@@ -141,6 +141,7 @@ static const char *android_perms[] = {
"MOUNT_UNMOUNT_FILESYSTEMS",
"NFC",
"PERSISTENT_ACTIVITY",
+ "POST_NOTIFICATIONS",
"PROCESS_OUTGOING_CALLS",
"READ_CALENDAR",
"READ_CALL_LOG",
@@ -379,14 +380,15 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
} else if (p.begins_with("ro.build.version.sdk=")) {
d.api_level = p.get_slice("=", 1).to_int();
} else if (p.begins_with("ro.product.cpu.abi=")) {
- d.description += "CPU: " + p.get_slice("=", 1).strip_edges() + "\n";
+ d.architecture = p.get_slice("=", 1).strip_edges();
+ d.description += "CPU: " + d.architecture + "\n";
} else if (p.begins_with("ro.product.manufacturer=")) {
d.description += "Manufacturer: " + p.get_slice("=", 1).strip_edges() + "\n";
} else if (p.begins_with("ro.board.platform=")) {
d.description += "Chipset: " + p.get_slice("=", 1).strip_edges() + "\n";
} else if (p.begins_with("ro.opengles.version=")) {
uint32_t opengl = p.get_slice("=", 1).to_int();
- d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl)&0xFF) + "\n";
+ d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl) & 0xFF) + "\n";
}
}
@@ -415,7 +417,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
}
}
- if (EDITOR_GET("export/android/shutdown_adb_on_exit")) {
+ if (ea->has_runnable_preset.is_set() && EDITOR_GET("export/android/shutdown_adb_on_exit")) {
String adb = get_adb_path();
if (!FileAccess::exists(adb)) {
return; //adb not configured
@@ -829,13 +831,82 @@ bool EditorExportPlatformAndroid::_uses_vulkan() {
void EditorExportPlatformAndroid::_notification(int p_what) {
#ifndef ANDROID_ENABLED
- if (p_what == NOTIFICATION_POSTINITIALIZE) {
- ERR_FAIL_NULL(EditorExport::get_singleton());
- EditorExport::get_singleton()->connect_presets_runnable_updated(callable_mp(this, &EditorExportPlatformAndroid::_update_preset_status));
+ switch (p_what) {
+ case NOTIFICATION_POSTINITIALIZE: {
+ if (EditorExport::get_singleton()) {
+ EditorExport::get_singleton()->connect_presets_runnable_updated(callable_mp(this, &EditorExportPlatformAndroid::_update_preset_status));
+ }
+ } break;
+
+ case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
+ if (EditorSettings::get_singleton()->check_changed_settings_in_group("export/android")) {
+ _create_editor_debug_keystore_if_needed();
+ }
+ } break;
}
#endif
}
+void EditorExportPlatformAndroid::_create_editor_debug_keystore_if_needed() {
+ // Check if we have a valid keytool path.
+ String keytool_path = get_keytool_path();
+ if (!FileAccess::exists(keytool_path)) {
+ return;
+ }
+
+ // Check if the current editor debug keystore exists.
+ String editor_debug_keystore = EDITOR_GET("export/android/debug_keystore");
+ if (FileAccess::exists(editor_debug_keystore)) {
+ return;
+ }
+
+ // Generate the debug keystore.
+ String keystore_path = EditorPaths::get_singleton()->get_debug_keystore_path();
+ String keystores_dir = keystore_path.get_base_dir();
+ if (!DirAccess::exists(keystores_dir)) {
+ Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ Error err = dir_access->make_dir_recursive(keystores_dir);
+ if (err != OK) {
+ WARN_PRINT(TTR("Error creating keystores directory:") + "\n" + keystores_dir);
+ return;
+ }
+ }
+
+ if (!FileAccess::exists(keystore_path)) {
+ String output;
+ List<String> args;
+ args.push_back("-genkey");
+ args.push_back("-keystore");
+ args.push_back(keystore_path);
+ args.push_back("-storepass");
+ args.push_back("android");
+ args.push_back("-alias");
+ args.push_back(DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
+ args.push_back("-keypass");
+ args.push_back(DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
+ args.push_back("-keyalg");
+ args.push_back("RSA");
+ args.push_back("-keysize");
+ args.push_back("2048");
+ args.push_back("-validity");
+ args.push_back("10000");
+ args.push_back("-dname");
+ args.push_back("cn=Godot, ou=Godot Engine, o=Stichting Godot, c=NL");
+ Error error = OS::get_singleton()->execute(keytool_path, args, &output, nullptr, true);
+ print_verbose(output);
+ if (error != OK) {
+ WARN_PRINT("Error: Unable to create debug keystore");
+ return;
+ }
+ }
+
+ // Update the editor settings.
+ EditorSettings::get_singleton()->set("export/android/debug_keystore", keystore_path);
+ EditorSettings::get_singleton()->set("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
+ EditorSettings::get_singleton()->set("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
+ print_verbose("Updated editor debug keystore to " + keystore_path);
+}
+
void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions) {
const char **aperms = android_perms;
while (*aperms) {
@@ -1390,6 +1461,14 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
p_manifest = ret;
}
+String EditorExportPlatformAndroid::_get_keystore_path(const Ref<EditorExportPreset> &p_preset, bool p_debug) {
+ String keystore_preference = p_debug ? "keystore/debug" : "keystore/release";
+ String keystore_env_variable = p_debug ? ENV_ANDROID_KEYSTORE_DEBUG_PATH : ENV_ANDROID_KEYSTORE_RELEASE_PATH;
+ String keystore_path = p_preset->get_or_env(keystore_preference, keystore_env_variable);
+
+ return ProjectSettings::get_singleton()->globalize_path(keystore_path).simplify_path();
+}
+
String EditorExportPlatformAndroid::_parse_string(const uint8_t *p_bytes, bool p_utf8) {
uint32_t offset = 0;
uint32_t len = 0;
@@ -1918,7 +1997,15 @@ bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExpor
bool advanced_options_enabled = p_preset->are_advanced_options_enabled();
if (p_option == "graphics/opengl_debug" ||
p_option == "command_line/extra_args" ||
- p_option == "permissions/custom_permissions") {
+ p_option == "permissions/custom_permissions" ||
+ p_option == "gradle_build/compress_native_libraries" ||
+ p_option == "package/retain_data_on_uninstall" ||
+ p_option == "package/exclude_from_recents" ||
+ p_option == "package/show_in_app_library" ||
+ p_option == "package/show_as_launcher_app" ||
+ p_option == "apk_expansion/enable" ||
+ p_option == "apk_expansion/SALT" ||
+ p_option == "apk_expansion/public_key") {
return advanced_options_enabled;
}
if (p_option == "gradle_build/gradle_build_directory" || p_option == "gradle_build/android_source_template") {
@@ -1928,6 +2015,12 @@ bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExpor
// The APK templates are ignored if Gradle build is enabled.
return advanced_options_enabled && !bool(p_preset->get("gradle_build/use_gradle_build"));
}
+
+ // Hide .NET embedding option (always enabled).
+ if (p_option == "dotnet/embed_build_outputs") {
+ return false;
+ }
+
return true;
}
@@ -1991,6 +2084,12 @@ String EditorExportPlatformAndroid::get_option_tooltip(int p_index) const {
return s;
}
+String EditorExportPlatformAndroid::get_device_architecture(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, devices.size(), "");
+ MutexLock lock(device_lock);
+ return devices[p_index].architecture;
+}
+
Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
ERR_FAIL_INDEX_V(p_device, devices.size(), ERR_INVALID_PARAMETER);
@@ -2179,6 +2278,15 @@ String EditorExportPlatformAndroid::get_java_path() {
return java_sdk_path.path_join("bin/java" + exe_ext);
}
+String EditorExportPlatformAndroid::get_keytool_path() {
+ String exe_ext;
+ if (OS::get_singleton()->get_name() == "Windows") {
+ exe_ext = ".exe";
+ }
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ return java_sdk_path.path_join("bin/keytool" + exe_ext);
+}
+
String EditorExportPlatformAndroid::get_adb_path() {
String exe_ext;
if (OS::get_singleton()->get_name() == "Windows") {
@@ -2324,10 +2432,10 @@ static bool has_valid_keystore_credentials(String &r_error_str, const String &p_
}
bool EditorExportPlatformAndroid::has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error) {
- String dk = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH);
+ String dk = _get_keystore_path(p_preset, true);
String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
- String rk = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH);
+ String rk = _get_keystore_path(p_preset, false);
String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
@@ -2400,9 +2508,22 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
err += template_err;
}
} else {
+ // Validate the custom gradle android source template.
+ bool android_source_template_valid = false;
+ const String android_source_template = p_preset->get("gradle_build/android_source_template");
+ if (!android_source_template.is_empty()) {
+ android_source_template_valid = FileAccess::exists(android_source_template);
+ if (!android_source_template_valid) {
+ err += TTR("Custom Android source template not found.") + "\n";
+ }
+ }
+
+ // Validate the installed build template.
bool installed_android_build_template = FileAccess::exists(ExportTemplateManager::get_android_build_directory(p_preset).path_join("build.gradle"));
if (!installed_android_build_template) {
- r_missing_templates = !exists_export_template("android_source.zip", &err);
+ if (!android_source_template_valid) {
+ r_missing_templates = !exists_export_template("android_source.zip", &err);
+ }
err += TTR("Android build template not installed in the project. Install it from the Project menu.") + "\n";
} else {
r_missing_templates = false;
@@ -2413,7 +2534,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
// Validate the rest of the export configuration.
- String dk = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH);
+ String dk = _get_keystore_path(p_preset, true);
String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
@@ -2431,7 +2552,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
}
}
- String rk = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH);
+ String rk = _get_keystore_path(p_preset, false);
String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
@@ -2688,7 +2809,7 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) {
int export_format = int(p_preset->get("gradle_build/export_format"));
String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK";
- String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH);
+ String release_keystore = _get_keystore_path(p_preset, false);
String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
@@ -2710,7 +2831,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
String password;
String user;
if (p_debug) {
- keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH);
+ keystore = _get_keystore_path(p_preset, true);
password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
@@ -3075,7 +3196,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
return err;
}
if (user_data.libs.size() > 0) {
- Ref<FileAccess> fa = FileAccess::open(GDEXTENSION_LIBS_PATH, FileAccess::WRITE);
+ Ref<FileAccess> fa = FileAccess::open(gdextension_libs_path, FileAccess::WRITE);
fa->store_string(JSON::stringify(user_data.libs, "\t"));
}
} else {
@@ -3195,7 +3316,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
if (should_sign) {
if (p_debug) {
- String debug_keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH);
+ String debug_keystore = _get_keystore_path(p_preset, true);
String debug_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
String debug_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
@@ -3217,7 +3338,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
cmdline.push_back("-Pdebug_keystore_password=" + debug_password); // argument to specify the debug keystore password.
} else {
// Pass the release keystore info as well
- String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH);
+ String release_keystore = _get_keystore_path(p_preset, false);
String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
if (release_keystore.is_relative_path()) {
@@ -3611,6 +3732,7 @@ EditorExportPlatformAndroid::EditorExportPlatformAndroid() {
android_plugins_changed.set();
#endif // DISABLE_DEPRECATED
#ifndef ANDROID_ENABLED
+ _create_editor_debug_keystore_if_needed();
_update_preset_status();
check_for_changes_thread.start(_check_for_changes_poll_thread, this);
#endif
diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h
index e25655c6cc..679afdc50f 100644
--- a/platform/android/export/export_plugin.h
+++ b/platform/android/export/export_plugin.h
@@ -60,6 +60,9 @@ const String ENV_ANDROID_KEYSTORE_RELEASE_PATH = "GODOT_ANDROID_KEYSTORE_RELEASE
const String ENV_ANDROID_KEYSTORE_RELEASE_USER = "GODOT_ANDROID_KEYSTORE_RELEASE_USER";
const String ENV_ANDROID_KEYSTORE_RELEASE_PASS = "GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD";
+const String DEFAULT_ANDROID_KEYSTORE_DEBUG_USER = "androiddebugkey";
+const String DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD = "android";
+
struct LauncherIcon {
const char *export_path;
int dimensions = 0;
@@ -76,6 +79,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
String name;
String description;
int api_level = 0;
+ String architecture;
};
struct APKExportData {
@@ -165,6 +169,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
void _fix_manifest(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_manifest, bool p_give_internet);
+ static String _get_keystore_path(const Ref<EditorExportPreset> &p_preset, bool p_debug);
+
static String _parse_string(const uint8_t *p_bytes, bool p_utf8);
void _fix_resources(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &r_manifest);
@@ -185,6 +191,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
const Ref<Image> &foreground,
const Ref<Image> &background);
+ static void _create_editor_debug_keystore_if_needed();
+
static Vector<ABI> get_enabled_abis(const Ref<EditorExportPreset> &p_preset);
static bool _uses_vulkan();
@@ -221,6 +229,8 @@ public:
virtual String get_option_tooltip(int p_index) const override;
+ virtual String get_device_architecture(int p_index) const override;
+
virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override;
virtual Ref<Texture2D> get_run_icon() const override;
@@ -231,6 +241,8 @@ public:
static String get_java_path();
+ static String get_keytool_path();
+
virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override;
virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override;
static bool has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error);
diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp
index f56eda4694..ae336d6f9d 100644
--- a/platform/android/file_access_android.cpp
+++ b/platform/android/file_access_android.cpp
@@ -31,8 +31,12 @@
#include "file_access_android.h"
#include "core/string/print_string.h"
+#include "thread_jandroid.h"
+
+#include <android/asset_manager_jni.h>
AAssetManager *FileAccessAndroid::asset_manager = nullptr;
+jobject FileAccessAndroid::j_asset_manager = nullptr;
String FileAccessAndroid::get_path() const {
return path_src;
@@ -257,3 +261,16 @@ void FileAccessAndroid::close() {
FileAccessAndroid::~FileAccessAndroid() {
_close();
}
+
+void FileAccessAndroid::setup(jobject p_asset_manager) {
+ JNIEnv *env = get_jni_env();
+ j_asset_manager = env->NewGlobalRef(p_asset_manager);
+ asset_manager = AAssetManager_fromJava(env, j_asset_manager);
+}
+
+void FileAccessAndroid::terminate() {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(j_asset_manager);
+}
diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h
index ec613b6687..e79daeafb3 100644
--- a/platform/android/file_access_android.h
+++ b/platform/android/file_access_android.h
@@ -35,9 +35,13 @@
#include <android/asset_manager.h>
#include <android/log.h>
+#include <jni.h>
#include <stdio.h>
class FileAccessAndroid : public FileAccess {
+ static AAssetManager *asset_manager;
+ static jobject j_asset_manager;
+
mutable AAsset *asset = nullptr;
mutable uint64_t len = 0;
mutable uint64_t pos = 0;
@@ -48,8 +52,6 @@ class FileAccessAndroid : public FileAccess {
void _close();
public:
- static AAssetManager *asset_manager;
-
virtual Error open_internal(const String &p_path, int p_mode_flags) override; // open a file
virtual bool is_open() const override; // true when file is open
@@ -65,6 +67,7 @@ public:
virtual bool eof_reached() const override; // reading passed EOF
+ virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; }
virtual uint8_t get_8() const override; // get a byte
virtual uint16_t get_16() const override;
virtual uint32_t get_32() const override;
@@ -92,6 +95,10 @@ public:
virtual void close() override;
+ static void setup(jobject p_asset_manager);
+
+ static void terminate();
+
~FileAccessAndroid();
};
diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp
index 46d9728632..f28d469d07 100644
--- a/platform/android/file_access_filesystem_jandroid.cpp
+++ b/platform/android/file_access_filesystem_jandroid.cpp
@@ -53,6 +53,7 @@ jmethodID FileAccessFilesystemJAndroid::_file_write = nullptr;
jmethodID FileAccessFilesystemJAndroid::_file_flush = nullptr;
jmethodID FileAccessFilesystemJAndroid::_file_exists = nullptr;
jmethodID FileAccessFilesystemJAndroid::_file_last_modified = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_resize = nullptr;
String FileAccessFilesystemJAndroid::get_path() const {
return path_src;
@@ -82,7 +83,7 @@ Error FileAccessFilesystemJAndroid::open_internal(const String &p_path, int p_mo
default:
return ERR_FILE_CANT_OPEN;
- case -1:
+ case -2:
return ERR_FILE_NOT_FOUND;
}
}
@@ -324,6 +325,30 @@ Error FileAccessFilesystemJAndroid::get_error() const {
return OK;
}
+Error FileAccessFilesystemJAndroid::resize(int64_t p_length) {
+ if (_file_resize) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL_V(env, FAILED);
+ ERR_FAIL_COND_V_MSG(!is_open(), FAILED, "File must be opened before use.");
+ int res = env->CallIntMethod(file_access_handler, _file_resize, id, p_length);
+ switch (res) {
+ case 0:
+ return OK;
+ case -4:
+ return ERR_INVALID_PARAMETER;
+ case -3:
+ return ERR_FILE_CANT_OPEN;
+ case -2:
+ return ERR_FILE_NOT_FOUND;
+ case -1:
+ default:
+ return FAILED;
+ }
+ } else {
+ return ERR_UNAVAILABLE;
+ }
+}
+
void FileAccessFilesystemJAndroid::flush() {
if (_file_flush) {
JNIEnv *env = get_jni_env();
@@ -383,6 +408,15 @@ void FileAccessFilesystemJAndroid::setup(jobject p_file_access_handler) {
_file_flush = env->GetMethodID(cls, "fileFlush", "(I)V");
_file_exists = env->GetMethodID(cls, "fileExists", "(Ljava/lang/String;)Z");
_file_last_modified = env->GetMethodID(cls, "fileLastModified", "(Ljava/lang/String;)J");
+ _file_resize = env->GetMethodID(cls, "fileResize", "(IJ)I");
+}
+
+void FileAccessFilesystemJAndroid::terminate() {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(cls);
+ env->DeleteGlobalRef(file_access_handler);
}
void FileAccessFilesystemJAndroid::close() {
diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h
index f33aa64ebe..6a8fc524b7 100644
--- a/platform/android/file_access_filesystem_jandroid.h
+++ b/platform/android/file_access_filesystem_jandroid.h
@@ -52,6 +52,7 @@ class FileAccessFilesystemJAndroid : public FileAccess {
static jmethodID _file_close;
static jmethodID _file_exists;
static jmethodID _file_last_modified;
+ static jmethodID _file_resize;
int id;
String absolute_path;
@@ -76,6 +77,7 @@ public:
virtual bool eof_reached() const override; ///< reading passed EOF
+ virtual Error resize(int64_t p_length) override;
virtual uint8_t get_8() const override; ///< get a byte
virtual uint16_t get_16() const override;
virtual uint32_t get_32() const override;
@@ -95,6 +97,7 @@ public:
virtual bool file_exists(const String &p_path) override; ///< return true if a file exists
static void setup(jobject p_file_access_handler);
+ static void terminate();
virtual uint64_t _get_modified_time(const String &p_file) override;
virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; }
diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle
index 7797f4bc9d..b83ef1471c 100644
--- a/platform/android/java/app/build.gradle
+++ b/platform/android/java/app/build.gradle
@@ -205,36 +205,66 @@ android {
}
task copyAndRenameDebugApk(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/apk/debug/android_debug.apk"
into getExportPath()
rename "android_debug.apk", getExportFilename()
}
task copyAndRenameDevApk(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/apk/dev/android_dev.apk"
into getExportPath()
rename "android_dev.apk", getExportFilename()
}
task copyAndRenameReleaseApk(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/apk/release/android_release.apk"
into getExportPath()
rename "android_release.apk", getExportFilename()
}
task copyAndRenameDebugAab(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/bundle/debug/build-debug.aab"
into getExportPath()
rename "build-debug.aab", getExportFilename()
}
task copyAndRenameDevAab(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/bundle/dev/build-dev.aab"
into getExportPath()
rename "build-dev.aab", getExportFilename()
}
task copyAndRenameReleaseAab(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/bundle/release/build-release.aab"
into getExportPath()
rename "build-release.aab", getExportFilename()
diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle
index f2c4a5d1b6..d27e75b07a 100644
--- a/platform/android/java/app/config.gradle
+++ b/platform/android/java/app/config.gradle
@@ -194,17 +194,17 @@ final String VALUE_SEPARATOR_REGEX = "\\|"
// get the list of ABIs the project should be exported to
ext.getExportEnabledABIs = { ->
- String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : "";
+ String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : ""
if (enabledABIs == null || enabledABIs.isEmpty()) {
enabledABIs = "armeabi-v7a|arm64-v8a|x86|x86_64|"
}
- Set<String> exportAbiFilter = [];
+ Set<String> exportAbiFilter = []
for (String abi_name : enabledABIs.split(VALUE_SEPARATOR_REGEX)) {
if (!abi_name.trim().isEmpty()){
- exportAbiFilter.add(abi_name);
+ exportAbiFilter.add(abi_name)
}
}
- return exportAbiFilter;
+ return exportAbiFilter
}
ext.getExportPath = {
diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle
index 0f7ffeecae..c5ef086152 100644
--- a/platform/android/java/editor/build.gradle
+++ b/platform/android/java/editor/build.gradle
@@ -20,7 +20,7 @@ ext {
String versionStatus = System.getenv("GODOT_VERSION_STATUS")
if (versionStatus != null && !versionStatus.isEmpty()) {
try {
- buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", ""));
+ buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", ""))
} catch (NumberFormatException ignored) {
buildNumber = 0
}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
index caf64bc933..c9a62d24b7 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
@@ -127,7 +127,7 @@ open class GodotEditor : GodotActivity() {
*/
protected open fun checkForProjectPermissionsToEnable() {
// Check for RECORD_AUDIO permission
- val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"));
+ val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
if (audioInputEnabled) {
PermissionsUtil.requestPermission(Manifest.permission.RECORD_AUDIO, this)
}
diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle
index ed967b9660..81ab598b90 100644
--- a/platform/android/java/lib/build.gradle
+++ b/platform/android/java/lib/build.gradle
@@ -11,6 +11,8 @@ apply from: "../scripts/publish-module.gradle"
dependencies {
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
+
+ testImplementation "junit:junit:4.13.2"
}
def pathToRootDir = "../../../../"
@@ -74,6 +76,7 @@ android {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
+ test.java.srcDirs = ['srcTest/java']
res.srcDirs = ['res']
aidl.srcDirs = ['aidl']
assets.srcDirs = ['assets']
@@ -118,7 +121,7 @@ android {
case "dev":
default:
sconsTarget += "_debug"
- break;
+ break
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
index e2e77e7796..ce53aeebcb 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
@@ -56,6 +56,7 @@ import org.godotengine.godot.io.directory.DirectoryAccessHandler
import org.godotengine.godot.io.file.FileAccessHandler
import org.godotengine.godot.plugin.GodotPluginRegistry
import org.godotengine.godot.tts.GodotTTS
+import org.godotengine.godot.utils.CommandLineFileParser
import org.godotengine.godot.utils.GodotNetUtils
import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.PermissionsUtil.requestPermission
@@ -68,7 +69,7 @@ import org.godotengine.godot.xr.XRMode
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
-import java.nio.charset.StandardCharsets
+import java.lang.Exception
import java.security.MessageDigest
import java.util.*
@@ -84,6 +85,9 @@ class Godot(private val context: Context) : SensorEventListener {
private val TAG = Godot::class.java.simpleName
}
+ private val windowManager: WindowManager by lazy {
+ requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ }
private val pluginRegistry: GodotPluginRegistry by lazy {
GodotPluginRegistry.getPluginRegistry()
}
@@ -120,6 +124,7 @@ class Godot(private val context: Context) : SensorEventListener {
val directoryAccessHandler = DirectoryAccessHandler(context)
val fileAccessHandler = FileAccessHandler(context)
val netUtils = GodotNetUtils(context)
+ private val commandLineFileParser = CommandLineFileParser()
/**
* Tracks whether [onCreate] was completed successfully.
@@ -150,7 +155,7 @@ class Godot(private val context: Context) : SensorEventListener {
private var useApkExpansion = false
private var useImmersive = false
private var useDebugOpengl = false
- private var darkMode = false;
+ private var darkMode = false
private var containerLayout: FrameLayout? = null
var renderView: GodotRenderView? = null
@@ -290,7 +295,7 @@ class Godot(private val context: Context) : SensorEventListener {
initializationStarted = false
throw e
} finally {
- endBenchmarkMeasure("Startup", "Godot::onCreate");
+ endBenchmarkMeasure("Startup", "Godot::onCreate")
}
}
@@ -396,16 +401,19 @@ class Godot(private val context: Context) : SensorEventListener {
}
if (host == primaryHost) {
- renderView!!.startRenderer()
+ renderView?.startRenderer()
}
- val view: View = renderView!!.view
- containerLayout?.addView(
- view,
+
+ renderView?.let {
+ containerLayout?.addView(
+ it.view,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
- )
+ )
+ }
+
editText.setView(renderView)
io?.setEdit(editText)
@@ -448,20 +456,23 @@ class Godot(private val context: Context) : SensorEventListener {
})
} else {
// Infer the virtual keyboard height using visible area.
- view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
+ renderView?.view?.viewTreeObserver?.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
// Don't allocate a new Rect every time the callback is called.
val visibleSize = Rect()
override fun onGlobalLayout() {
- val surfaceView = renderView!!.view
- surfaceView.getWindowVisibleDisplayFrame(visibleSize)
- val keyboardHeight = surfaceView.height - visibleSize.bottom
- GodotLib.setVirtualKeyboardHeight(keyboardHeight)
+ renderView?.let {
+ val surfaceView = it.view
+
+ surfaceView.getWindowVisibleDisplayFrame(visibleSize)
+ val keyboardHeight = surfaceView.height - visibleSize.bottom
+ GodotLib.setVirtualKeyboardHeight(keyboardHeight)
+ }
}
})
}
if (host == primaryHost) {
- renderView!!.queueOnRenderThread {
+ renderView?.queueOnRenderThread {
for (plugin in pluginRegistry.allPlugins) {
plugin.onRegisterPluginWithGodotNative()
}
@@ -495,7 +506,7 @@ class Godot(private val context: Context) : SensorEventListener {
return
}
- renderView!!.onActivityStarted()
+ renderView?.onActivityStarted()
}
fun onResume(host: GodotHost) {
@@ -503,7 +514,7 @@ class Godot(private val context: Context) : SensorEventListener {
return
}
- renderView!!.onActivityResumed()
+ renderView?.onActivityResumed()
if (mAccelerometer != null) {
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
}
@@ -535,7 +546,7 @@ class Godot(private val context: Context) : SensorEventListener {
return
}
- renderView!!.onActivityPaused()
+ renderView?.onActivityPaused()
mSensorManager.unregisterListener(this)
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainPause()
@@ -547,7 +558,7 @@ class Godot(private val context: Context) : SensorEventListener {
return
}
- renderView!!.onActivityStopped()
+ renderView?.onActivityStopped()
}
fun onDestroy(primaryHost: GodotHost) {
@@ -569,7 +580,7 @@ class Godot(private val context: Context) : SensorEventListener {
* Configuration change callback
*/
fun onConfigurationChanged(newConfig: Configuration) {
- var newDarkMode = newConfig.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
+ val newDarkMode = newConfig.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
if (darkMode != newDarkMode) {
darkMode = newDarkMode
GodotLib.onNightModeChanged()
@@ -613,7 +624,7 @@ class Godot(private val context: Context) : SensorEventListener {
// These properties are defined after Godot setup completion, so we retrieve them here.
val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click"))
val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"))
- val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis"));
+ val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis"))
runOnUiThread {
renderView?.inputHandler?.apply {
@@ -686,9 +697,7 @@ class Godot(private val context: Context) : SensorEventListener {
* This must be called after the render thread has started.
*/
fun runOnRenderThread(action: Runnable) {
- if (renderView != null) {
- renderView!!.queueOnRenderThread(action)
- }
+ renderView?.queueOnRenderThread(action)
}
/**
@@ -765,7 +774,7 @@ class Godot(private val context: Context) : SensorEventListener {
return mClipboard.hasPrimaryClip()
}
- fun getClipboard(): String? {
+ fun getClipboard(): String {
val clipData = mClipboard.primaryClip ?: return ""
val text = clipData.getItemAt(0).text ?: return ""
return text.toString()
@@ -782,15 +791,14 @@ class Godot(private val context: Context) : SensorEventListener {
@Keep
private fun forceQuit(instanceId: Int): Boolean {
- if (primaryHost == null) {
- return false
- }
- return if (instanceId == 0) {
- primaryHost!!.onGodotForceQuit(this)
- true
- } else {
- primaryHost!!.onGodotForceQuit(instanceId)
- }
+ primaryHost?.let {
+ if (instanceId == 0) {
+ it.onGodotForceQuit(this)
+ return true
+ } else {
+ return it.onGodotForceQuit(instanceId)
+ }
+ } ?: return false
}
fun onBackPressed(host: GodotHost) {
@@ -804,20 +812,17 @@ class Godot(private val context: Context) : SensorEventListener {
shouldQuit = false
}
}
- if (shouldQuit && renderView != null) {
- renderView!!.queueOnRenderThread { GodotLib.back() }
+ if (shouldQuit) {
+ renderView?.queueOnRenderThread { GodotLib.back() }
}
}
private fun getRotatedValues(values: FloatArray?): FloatArray? {
if (values == null || values.size != 3) {
- return values
+ return null
}
- val display =
- (requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
- val displayRotation = display.rotation
val rotatedValues = FloatArray(3)
- when (displayRotation) {
+ when (windowManager.defaultDisplay.rotation) {
Surface.ROTATION_0 -> {
rotatedValues[0] = values[0]
rotatedValues[1] = values[1]
@@ -846,37 +851,36 @@ class Godot(private val context: Context) : SensorEventListener {
if (renderView == null) {
return
}
+
+ val rotatedValues = getRotatedValues(event.values)
+
when (event.sensor.type) {
Sensor.TYPE_ACCELEROMETER -> {
- val rotatedValues = getRotatedValues(event.values)
- renderView!!.queueOnRenderThread {
- GodotLib.accelerometer(
- -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
- )
+ rotatedValues?.let {
+ renderView?.queueOnRenderThread {
+ GodotLib.accelerometer(-it[0], -it[1], -it[2])
+ }
}
}
Sensor.TYPE_GRAVITY -> {
- val rotatedValues = getRotatedValues(event.values)
- renderView!!.queueOnRenderThread {
- GodotLib.gravity(
- -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
- )
+ rotatedValues?.let {
+ renderView?.queueOnRenderThread {
+ GodotLib.gravity(-it[0], -it[1], -it[2])
+ }
}
}
Sensor.TYPE_MAGNETIC_FIELD -> {
- val rotatedValues = getRotatedValues(event.values)
- renderView!!.queueOnRenderThread {
- GodotLib.magnetometer(
- -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
- )
+ rotatedValues?.let {
+ renderView?.queueOnRenderThread {
+ GodotLib.magnetometer(-it[0], -it[1], -it[2])
+ }
}
}
Sensor.TYPE_GYROSCOPE -> {
- val rotatedValues = getRotatedValues(event.values)
- renderView!!.queueOnRenderThread {
- GodotLib.gyroscope(
- rotatedValues!![0], rotatedValues[1], rotatedValues[2]
- )
+ rotatedValues?.let {
+ renderView?.queueOnRenderThread {
+ GodotLib.gyroscope(it[0], it[1], it[2])
+ }
}
}
}
@@ -908,47 +912,18 @@ class Godot(private val context: Context) : SensorEventListener {
}
private fun getCommandLine(): MutableList<String> {
- val original: MutableList<String> = parseCommandLine()
+ val commandLine = try {
+ commandLineFileParser.parseCommandLine(requireActivity().assets.open("_cl_"))
+ } catch (ignored: Exception) {
+ mutableListOf()
+ }
+
val hostCommandLine = primaryHost?.commandLine
if (!hostCommandLine.isNullOrEmpty()) {
- original.addAll(hostCommandLine)
+ commandLine.addAll(hostCommandLine)
}
- return original
- }
- private fun parseCommandLine(): MutableList<String> {
- val inputStream: InputStream
- return try {
- inputStream = requireActivity().assets.open("_cl_")
- val len = ByteArray(4)
- var r = inputStream.read(len)
- if (r < 4) {
- return mutableListOf()
- }
- val argc =
- (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF)
- val cmdline = ArrayList<String>(argc)
- for (i in 0 until argc) {
- r = inputStream.read(len)
- if (r < 4) {
- return mutableListOf()
- }
- val strlen =
- (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF)
- if (strlen > 65535) {
- return mutableListOf()
- }
- val arg = ByteArray(strlen)
- r = inputStream.read(arg)
- if (r == strlen) {
- cmdline.add(String(arg, StandardCharsets.UTF_8))
- }
- }
- cmdline
- } catch (e: Exception) {
- // The _cl_ file can be missing with no adverse effect
- mutableListOf()
- }
+ return commandLine
}
/**
@@ -1039,7 +1014,7 @@ class Godot(private val context: Context) : SensorEventListener {
@Keep
private fun initInputDevices() {
- renderView!!.initInputDevices()
+ renderView?.initInputDevices()
}
@Keep
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
index e01c5481d5..7b8fad8952 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
@@ -83,8 +83,9 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
override fun onDestroy() {
Log.v(TAG, "Destroying Godot app...")
super.onDestroy()
- if (godotFragment != null) {
- terminateGodotInstance(godotFragment!!.godot)
+
+ godotFragment?.let {
+ terminateGodotInstance(it.godot)
}
}
@@ -93,22 +94,26 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
}
private fun terminateGodotInstance(instance: Godot) {
- if (godotFragment != null && instance === godotFragment!!.godot) {
- Log.v(TAG, "Force quitting Godot instance")
- ProcessPhoenix.forceQuit(this)
+ godotFragment?.let {
+ if (instance === it.godot) {
+ Log.v(TAG, "Force quitting Godot instance")
+ ProcessPhoenix.forceQuit(this)
+ }
}
}
override fun onGodotRestartRequested(instance: Godot) {
runOnUiThread {
- if (godotFragment != null && instance === godotFragment!!.godot) {
- // It's very hard to properly de-initialize Godot on Android to restart the game
- // from scratch. Therefore, we need to kill the whole app process and relaunch it.
- //
- // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
- // releasing and reloading native libs or resetting their state somehow and clearing static data).
- Log.v(TAG, "Restarting Godot instance...")
- ProcessPhoenix.triggerRebirth(this)
+ godotFragment?.let {
+ if (instance === it.godot) {
+ // It's very hard to properly de-initialize Godot on Android to restart the game
+ // from scratch. Therefore, we need to kill the whole app process and relaunch it.
+ //
+ // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
+ // releasing and reloading native libs or resetting their state somehow and clearing static data).
+ Log.v(TAG, "Restarting Godot instance...")
+ ProcessPhoenix.triggerRebirth(this)
+ }
}
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java
index ef97aaeab9..bd8c58ad69 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java
@@ -1673,7 +1673,24 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
mWantRenderNotification = true;
mRequestRender = true;
mRenderComplete = false;
- mFinishDrawingRunnable = finishDrawing;
+
+ // fix lost old callback when continuous call requestRenderAndNotify
+ //
+ // If continuous call requestRenderAndNotify before trigger old
+ // callback, old callback will lose, cause VRI will wait for SV's
+ // draw to finish forever not calling finishDraw.
+ // https://android.googlesource.com/platform/frameworks/base/+/044fce0b826f2da3a192aac56785b5089143e693%5E%21/
+ //+++++++++++++++++++++++++++++++++++++++++++++++++++
+ final Runnable oldCallback = mFinishDrawingRunnable;
+ mFinishDrawingRunnable = () -> {
+ if (oldCallback != null) {
+ oldCallback.run();
+ }
+ if (finishDrawing != null) {
+ finishDrawing.run();
+ }
+ };
+ //----------------------------------------------------
sGLThreadManager.notifyAll();
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
index 0f447f0b05..11cf7b3566 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
@@ -36,7 +36,9 @@ import android.util.Log
import org.godotengine.godot.io.StorageScope
import java.io.IOException
import java.nio.ByteBuffer
+import java.nio.channels.ClosedChannelException
import java.nio.channels.FileChannel
+import java.nio.channels.NonWritableChannelException
import kotlin.math.max
/**
@@ -135,6 +137,21 @@ internal abstract class DataAccess(private val filePath: String) {
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
+ }
+ }
+
fun position(): Long {
return try {
fileChannel.position()
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
index 984bf607d0..1d773467e8 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
@@ -45,7 +45,6 @@ class FileAccessHandler(val context: Context) {
companion object {
private val TAG = FileAccessHandler::class.java.simpleName
- private const val FILE_NOT_FOUND_ERROR_ID = -1
internal const val INVALID_FILE_ID = 0
private const val STARTING_FILE_ID = 1
@@ -56,7 +55,9 @@ class FileAccessHandler(val context: Context) {
}
return try {
- DataAccess.fileExists(storageScope, context, path!!)
+ path?.let {
+ DataAccess.fileExists(storageScope, context, it)
+ } ?: false
} catch (e: SecurityException) {
false
}
@@ -69,20 +70,22 @@ class FileAccessHandler(val context: Context) {
}
return try {
- DataAccess.removeFile(storageScope, context, path!!)
+ path?.let {
+ DataAccess.removeFile(storageScope, context, it)
+ } ?: false
} catch (e: Exception) {
false
}
}
- internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String?, to: String?): Boolean {
+ internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String, to: String): Boolean {
val storageScope = storageScopeIdentifier.identifyStorageScope(from)
if (storageScope == StorageScope.UNKNOWN) {
return false
}
return try {
- DataAccess.renameFile(storageScope, context, from!!, to!!)
+ DataAccess.renameFile(storageScope, context, from, to)
} catch (e: Exception) {
false
}
@@ -106,16 +109,18 @@ class FileAccessHandler(val context: Context) {
return INVALID_FILE_ID
}
- try {
- val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID
+ return try {
+ path?.let {
+ val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return INVALID_FILE_ID
- files.put(++lastFileId, dataAccess)
- return lastFileId
+ files.put(++lastFileId, dataAccess)
+ lastFileId
+ } ?: INVALID_FILE_ID
} catch (e: FileNotFoundException) {
- return FILE_NOT_FOUND_ERROR_ID
+ FileErrors.FILE_NOT_FOUND.nativeValue
} catch (e: Exception) {
Log.w(TAG, "Error while opening $path", e)
- return INVALID_FILE_ID
+ INVALID_FILE_ID
}
}
@@ -176,12 +181,22 @@ class FileAccessHandler(val context: Context) {
}
return try {
- DataAccess.fileLastModified(storageScope, context, filepath!!)
+ filepath?.let {
+ DataAccess.fileLastModified(storageScope, context, it)
+ } ?: 0L
} catch (e: SecurityException) {
0L
}
}
+ fun fileResize(fileId: Int, length: Long): Int {
+ if (!hasFileId(fileId)) {
+ return FileErrors.FAILED.nativeValue
+ }
+
+ return files[fileId].resize(length)
+ }
+
fun fileGetPosition(fileId: Int): Long {
if (!hasFileId(fileId)) {
return 0L
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt
new file mode 100644
index 0000000000..2df0195de7
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt
@@ -0,0 +1,53 @@
+/**************************************************************************/
+/* FileErrors.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
+
+/**
+ * Set of errors that may occur when performing data access.
+ */
+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
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt
new file mode 100644
index 0000000000..ce5c5b6714
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt
@@ -0,0 +1,83 @@
+/**************************************************************************/
+/* CommandLineFileParser.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 java.io.InputStream
+import java.nio.charset.StandardCharsets
+import java.util.ArrayList
+
+/**
+ * A class that parses the content of file storing command line params. Usually, this file is saved
+ * in `assets/_cl_` on exporting an apk
+ *
+ * Returns a mutable list of command lines
+ */
+internal class CommandLineFileParser {
+ fun parseCommandLine(inputStream: InputStream): MutableList<String> {
+ return try {
+ val headerBytes = ByteArray(4)
+ var argBytes = inputStream.read(headerBytes)
+ if (argBytes < 4) {
+ return mutableListOf()
+ }
+ val argc = decodeHeaderIntValue(headerBytes)
+
+ val cmdline = ArrayList<String>(argc)
+ for (i in 0 until argc) {
+ argBytes = inputStream.read(headerBytes)
+ if (argBytes < 4) {
+ return mutableListOf()
+ }
+ val strlen = decodeHeaderIntValue(headerBytes)
+
+ if (strlen > 65535) {
+ return mutableListOf()
+ }
+
+ val arg = ByteArray(strlen)
+ argBytes = inputStream.read(arg)
+ if (argBytes == strlen) {
+ cmdline.add(String(arg, StandardCharsets.UTF_8))
+ }
+ }
+ cmdline
+ } catch (e: Exception) {
+ // The _cl_ file can be missing with no adverse effect
+ mutableListOf()
+ }
+ }
+
+ private fun decodeHeaderIntValue(headerBytes: ByteArray): Int =
+ (headerBytes[3].toInt() and 0xFF) shl 24 or
+ ((headerBytes[2].toInt() and 0xFF) shl 16) or
+ ((headerBytes[1].toInt() and 0xFF) shl 8) or
+ (headerBytes[0].toInt() and 0xFF)
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java
index 737b4ac20b..9df890e6bd 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java
@@ -41,12 +41,16 @@ import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.Settings;
+import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -67,12 +71,74 @@ public final class PermissionsUtil {
}
/**
- * Request a dangerous permission. name must be specified in <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/res/AndroidManifest.xml">this</a>
- * @param permissionName the name of the requested permission.
+ * Request a list of dangerous permissions. The requested permissions must be included in the app's AndroidManifest
+ * @param permissions list of the permissions to request.
+ * @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;
+ }
+
+ if (permissions == null || permissions.isEmpty()) {
+ return true;
+ }
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // Not necessary, asked on install already
+ return true;
+ }
+
+ Set<String> requestedPermissions = new HashSet<>();
+ for (String permission : permissions) {
+ try {
+ if (permission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
+ Log.d(TAG, "Requesting permission " + permission);
+ try {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
+ intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
+ activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
+ } catch (Exception ignored) {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
+ activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
+ }
+ }
+ } 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) {
+ Log.d(TAG, "Requesting permission " + permission);
+ requestedPermissions.add(permission);
+ }
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // Skip this permission and continue.
+ Log.w(TAG, "Unable to identify permission " + permission, e);
+ }
+ }
+
+ if (requestedPermissions.isEmpty()) {
+ // If list is empty, all of dangerous permissions were granted.
+ return true;
+ }
+
+ activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE);
+ return true;
+ }
+
+ /**
+ * Request a dangerous permission. The requested permission must be included in the app's AndroidManifest
+ * @param permissionName the name of the permission to request.
* @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
return true;
@@ -137,11 +203,15 @@ public final class PermissionsUtil {
* @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched.
*/
public static boolean requestManifestPermissions(Activity activity, @Nullable Set<String> excludes) {
+ if (activity == null) {
+ return false;
+ }
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
- String[] manifestPermissions;
+ List<String> manifestPermissions;
try {
manifestPermissions = getManifestPermissions(activity);
} catch (PackageManager.NameNotFoundException e) {
@@ -149,48 +219,17 @@ public final class PermissionsUtil {
return false;
}
- if (manifestPermissions.length == 0)
+ if (manifestPermissions.isEmpty()) {
return true;
-
- List<String> requestedPermissions = new ArrayList<>();
- for (String manifestPermission : manifestPermissions) {
- if (excludes != null && excludes.contains(manifestPermission)) {
- continue;
- }
- try {
- if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
- Log.d(TAG, "Requesting permission " + manifestPermission);
- try {
- Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
- intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
- activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
- } catch (Exception ignored) {
- Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
- activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
- }
- }
- } else {
- PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
- int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
- if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) {
- Log.d(TAG, "Requesting permission " + manifestPermission);
- requestedPermissions.add(manifestPermission);
- }
- }
- } catch (PackageManager.NameNotFoundException e) {
- // Skip this permission and continue.
- Log.w(TAG, "Unable to identify permission " + manifestPermission, e);
- }
}
- if (requestedPermissions.isEmpty()) {
- // If list is empty, all of dangerous permissions were granted.
- return true;
+ if (excludes != null && !excludes.isEmpty()) {
+ for (String excludedPermission : excludes) {
+ manifestPermissions.remove(excludedPermission);
+ }
}
- activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE);
- return false;
+ return requestPermissions(activity, manifestPermissions);
}
/**
@@ -199,15 +238,16 @@ public final class PermissionsUtil {
* @return granted permissions list
*/
public static String[] getGrantedPermissions(Context context) {
- String[] manifestPermissions;
+ List<String> manifestPermissions;
try {
manifestPermissions = getManifestPermissions(context);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return new String[0];
}
- if (manifestPermissions.length == 0)
- return manifestPermissions;
+ if (manifestPermissions.isEmpty()) {
+ return new String[0];
+ }
List<String> grantedPermissions = new ArrayList<>();
for (String manifestPermission : manifestPermissions) {
@@ -253,15 +293,15 @@ public final class PermissionsUtil {
/**
* Returns the permissions defined in the AndroidManifest.xml file.
* @param context the caller context for this method.
- * @return manifest permissions list
+ * @return mutable copy of manifest permissions list
* @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found.
*/
- private static String[] getManifestPermissions(Context context) throws PackageManager.NameNotFoundException {
+ public static ArrayList<String> getManifestPermissions(Context context) throws PackageManager.NameNotFoundException {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
if (packageInfo.requestedPermissions == null)
- return new String[0];
- return packageInfo.requestedPermissions;
+ return new ArrayList<String>();
+ return new ArrayList<>(Arrays.asList(packageInfo.requestedPermissions));
}
/**
diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt
index 4aba0c370d..8c0065b31e 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt
@@ -142,7 +142,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
fun onSurfaceChanged(width: Int, height: Int) {
lock.withLock {
hasSurface = true
- surfaceChanged = true;
+ surfaceChanged = true
this.width = width
this.height = height
@@ -179,7 +179,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
// blocking the thread lifecycle by holding onto the lock.
if (eventQueue.isNotEmpty()) {
event = eventQueue.removeAt(0)
- break;
+ break
}
if (readyToDraw) {
@@ -199,7 +199,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
}
// Break out of the loop so drawing can occur without holding onto the lock.
- break;
+ break
} else if (rendererResumed) {
// If we aren't ready to draw but are resumed, that means we either lost a surface
// or the app was paused.
diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java
index 01ee41e30b..1f0d8592b3 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java
@@ -66,11 +66,15 @@ public class RegularContextFactory implements GLSurfaceView.EGLContextFactory {
GLUtils.checkEglError(TAG, "Before eglCreateContext", egl);
EGLContext context;
+ int[] debug_attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE };
+ int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE };
if (mUseDebugOpengl) {
- int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE };
- context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
+ context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, debug_attrib_list);
+ if (context == null || context == EGL10.EGL_NO_CONTEXT) {
+ Log.w(TAG, "creating 'OpenGL Debug' context failed");
+ context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
+ }
} else {
- int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE };
context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
}
GLUtils.checkEglError(TAG, "After eglCreateContext", egl);
diff --git a/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt b/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt
new file mode 100644
index 0000000000..8b0466848a
--- /dev/null
+++ b/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt
@@ -0,0 +1,104 @@
+/**************************************************************************/
+/* CommandLineFileParserTest.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 org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.io.ByteArrayInputStream
+import java.io.InputStream
+
+// Godot saves command line params in the `assets/_cl_` file on exporting an apk. By default,
+// without any other commands specified in `command_line/extra_args` in Export window, the content
+// of that _cl_ file consists of only the `--xr_mode_regular` and `--use_immersive` flags.
+// The `CL_` prefix here refers to that file
+private val CL_DEFAULT_NO_EXTRA_ARGS = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
+private val CL_ONE_EXTRA_ARG = byteArrayOf(3, 0, 0, 0, 15, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
+private val CL_TWO_EXTRA_ARGS = byteArrayOf(4, 0, 0, 0, 16, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 49, 16, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 50, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
+private val CL_EMPTY = byteArrayOf()
+private val CL_HEADER_TOO_SHORT = byteArrayOf(0, 0, 0)
+private val CL_INCOMPLETE_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0)
+private val CL_LENGTH_TOO_LONG_IN_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
+private val CL_MISMATCHED_ARG_LENGTH_AND_HEADER_ONE_ARG = byteArrayOf(2, 0, 0, 0, 10, 0, 0, 0, 45, 45, 120, 114)
+private val CL_MISMATCHED_ARG_LENGTH_AND_HEADER_IN_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
+
+@RunWith(Parameterized::class)
+class CommandLineFileParserTest(
+ private val inputStreamArg: InputStream,
+ private val expectedResult: List<String>,
+) {
+
+ private val commandLineFileParser = CommandLineFileParser()
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun data() = listOf(
+ arrayOf(ByteArrayInputStream(CL_EMPTY), listOf<String>()),
+ arrayOf(ByteArrayInputStream(CL_HEADER_TOO_SHORT), listOf<String>()),
+
+ arrayOf(ByteArrayInputStream(CL_DEFAULT_NO_EXTRA_ARGS), listOf(
+ "--xr_mode_regular",
+ "--use_immersive",
+ )),
+
+ arrayOf(ByteArrayInputStream(CL_ONE_EXTRA_ARG), listOf(
+ "--unit_test_arg",
+ "--xr_mode_regular",
+ "--use_immersive",
+ )),
+
+ arrayOf(ByteArrayInputStream(CL_TWO_EXTRA_ARGS), listOf(
+ "--unit_test_arg1",
+ "--unit_test_arg2",
+ "--xr_mode_regular",
+ "--use_immersive",
+ )),
+
+ arrayOf(ByteArrayInputStream(CL_INCOMPLETE_FIRST_ARG), listOf<String>()),
+ arrayOf(ByteArrayInputStream(CL_LENGTH_TOO_LONG_IN_FIRST_ARG), listOf<String>()),
+ arrayOf(ByteArrayInputStream(CL_MISMATCHED_ARG_LENGTH_AND_HEADER_ONE_ARG), listOf<String>()),
+ arrayOf(ByteArrayInputStream(CL_MISMATCHED_ARG_LENGTH_AND_HEADER_IN_FIRST_ARG), listOf<String>()),
+ )
+ }
+
+ @Test
+ fun `Given inputStream, When parsing command line, Then a correct list is returned`() {
+ // given
+ val inputStream = inputStreamArg
+
+ // when
+ val result = commandLineFileParser.parseCommandLine(inputStream)
+
+ // then
+ assert(result == expectedResult) { "Expected: $expectedResult Actual: $result" }
+ }
+}
diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp
index d6455cbf1c..a309a6ab74 100644
--- a/platform/android/java_class_wrapper.cpp
+++ b/platform/android/java_class_wrapper.cpp
@@ -1157,50 +1157,54 @@ JavaClassWrapper::JavaClassWrapper(jobject p_activity) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL(env);
- jclass activity = env->FindClass("android/app/Activity");
- jmethodID getClassLoader = env->GetMethodID(activity, "getClassLoader", "()Ljava/lang/ClassLoader;");
- classLoader = env->CallObjectMethod(p_activity, getClassLoader);
- classLoader = (jclass)env->NewGlobalRef(classLoader);
- jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
- findClass = env->GetMethodID(classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
-
jclass bclass = env->FindClass("java/lang/Class");
getDeclaredMethods = env->GetMethodID(bclass, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;");
getFields = env->GetMethodID(bclass, "getFields", "()[Ljava/lang/reflect/Field;");
Class_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/reflect/Method");
getParameterTypes = env->GetMethodID(bclass, "getParameterTypes", "()[Ljava/lang/Class;");
getReturnType = env->GetMethodID(bclass, "getReturnType", "()Ljava/lang/Class;");
getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;");
getModifiers = env->GetMethodID(bclass, "getModifiers", "()I");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/reflect/Field");
Field_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;");
Field_getModifiers = env->GetMethodID(bclass, "getModifiers", "()I");
Field_get = env->GetMethodID(bclass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Boolean");
Boolean_booleanValue = env->GetMethodID(bclass, "booleanValue", "()Z");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Byte");
Byte_byteValue = env->GetMethodID(bclass, "byteValue", "()B");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Character");
Character_characterValue = env->GetMethodID(bclass, "charValue", "()C");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Short");
Short_shortValue = env->GetMethodID(bclass, "shortValue", "()S");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Integer");
Integer_integerValue = env->GetMethodID(bclass, "intValue", "()I");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Long");
Long_longValue = env->GetMethodID(bclass, "longValue", "()J");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Float");
Float_floatValue = env->GetMethodID(bclass, "floatValue", "()F");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Double");
Double_doubleValue = env->GetMethodID(bclass, "doubleValue", "()D");
+ env->DeleteLocalRef(bclass);
}
diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp
index 10716a5c79..49913b9c30 100644
--- a/platform/android/java_godot_io_wrapper.cpp
+++ b/platform/android/java_godot_io_wrapper.cpp
@@ -70,7 +70,11 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc
}
GodotIOJavaWrapper::~GodotIOJavaWrapper() {
- // nothing to do here for now
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(cls);
+ env->DeleteGlobalRef(godot_io_instance);
}
jobject GodotIOJavaWrapper::get_instance() {
@@ -82,7 +86,9 @@ Error GodotIOJavaWrapper::open_uri(const String &p_uri) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, ERR_UNAVAILABLE);
jstring jStr = env->NewStringUTF(p_uri.utf8().get_data());
- return env->CallIntMethod(godot_io_instance, _open_URI, jStr) ? ERR_CANT_OPEN : OK;
+ Error result = env->CallIntMethod(godot_io_instance, _open_URI, jStr) ? ERR_CANT_OPEN : OK;
+ env->DeleteLocalRef(jStr);
+ return result;
} else {
return ERR_UNAVAILABLE;
}
@@ -220,6 +226,7 @@ void GodotIOJavaWrapper::show_vk(const String &p_existing, int p_type, int p_max
ERR_FAIL_NULL(env);
jstring jStr = env->NewStringUTF(p_existing.utf8().get_data());
env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_type, p_max_input_length, p_cursor_start, p_cursor_end);
+ env->DeleteLocalRef(jStr);
}
}
diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp
index 85d5cf2796..6cab7e74fd 100644
--- a/platform/android/java_godot_lib_jni.cpp
+++ b/platform/android/java_godot_lib_jni.cpp
@@ -95,6 +95,13 @@ static void _terminate(JNIEnv *env, bool p_restart = false) {
if (godot_io_java) {
delete godot_io_java;
}
+
+ TTS_Android::terminate();
+ FileAccessAndroid::terminate();
+ DirAccessJAndroid::terminate();
+ FileAccessFilesystemJAndroid::terminate();
+ NetSocketAndroid::terminate();
+
if (godot_java) {
if (!restart_on_cleanup) {
if (p_restart) {
@@ -125,10 +132,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv
init_thread_jandroid(jvm, env);
- jobject amgr = env->NewGlobalRef(p_asset_manager);
-
- FileAccessAndroid::asset_manager = AAssetManager_fromJava(env, amgr);
-
+ FileAccessAndroid::setup(p_asset_manager);
DirAccessJAndroid::setup(p_directory_access_handler);
FileAccessFilesystemJAndroid::setup(p_file_access_handler);
NetSocketAndroid::setup(p_net_utils);
@@ -250,7 +254,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env,
}
if (step.get() == 1) {
- if (!Main::start()) {
+ if (Main::start() != EXIT_SUCCESS) {
return true; // should exit instead and print the error
}
diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp
index a95f762e01..04424c1179 100644
--- a/platform/android/java_godot_view_wrapper.cpp
+++ b/platform/android/java_godot_view_wrapper.cpp
@@ -95,6 +95,7 @@ void GodotJavaViewWrapper::configure_pointer_icon(int pointer_type, const String
jstring jImagePath = env->NewStringUTF(image_path.utf8().get_data());
env->CallVoidMethod(_godot_view, _configure_pointer_icon, pointer_type, jImagePath, p_hotspot.x, p_hotspot.y);
+ env->DeleteLocalRef(jImagePath);
}
}
diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp
index 3c950bb1b1..61be6fc5db 100644
--- a/platform/android/java_godot_wrapper.cpp
+++ b/platform/android/java_godot_wrapper.cpp
@@ -172,6 +172,8 @@ void GodotJavaWrapper::alert(const String &p_message, const String &p_title) {
jstring jStrMessage = env->NewStringUTF(p_message.utf8().get_data());
jstring jStrTitle = env->NewStringUTF(p_title.utf8().get_data());
env->CallVoidMethod(godot_instance, _alert, jStrMessage, jStrTitle);
+ env->DeleteLocalRef(jStrMessage);
+ env->DeleteLocalRef(jStrTitle);
}
}
@@ -231,6 +233,7 @@ void GodotJavaWrapper::set_clipboard(const String &p_text) {
ERR_FAIL_NULL(env);
jstring jStr = env->NewStringUTF(p_text.utf8().get_data());
env->CallVoidMethod(godot_instance, _set_clipboard, jStr);
+ env->DeleteLocalRef(jStr);
}
}
@@ -253,7 +256,9 @@ bool GodotJavaWrapper::request_permission(const String &p_name) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, false);
jstring jStrName = env->NewStringUTF(p_name.utf8().get_data());
- return env->CallBooleanMethod(godot_instance, _request_permission, jStrName);
+ bool result = env->CallBooleanMethod(godot_instance, _request_permission, jStrName);
+ env->DeleteLocalRef(jStrName);
+ return result;
} else {
return false;
}
@@ -340,7 +345,9 @@ int GodotJavaWrapper::create_new_godot_instance(const List<String> &args) {
ERR_FAIL_NULL_V(env, 0);
jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF(""));
for (int i = 0; i < args.size(); i++) {
- env->SetObjectArrayElement(jargs, i, env->NewStringUTF(args[i].utf8().get_data()));
+ jstring j_arg = env->NewStringUTF(args[i].utf8().get_data());
+ env->SetObjectArrayElement(jargs, i, j_arg);
+ env->DeleteLocalRef(j_arg);
}
return env->CallIntMethod(godot_instance, _create_new_godot_instance, jargs);
} else {
@@ -355,6 +362,8 @@ void GodotJavaWrapper::begin_benchmark_measure(const String &p_context, const St
jstring j_context = env->NewStringUTF(p_context.utf8().get_data());
jstring j_label = env->NewStringUTF(p_label.utf8().get_data());
env->CallVoidMethod(godot_instance, _begin_benchmark_measure, j_context, j_label);
+ env->DeleteLocalRef(j_context);
+ env->DeleteLocalRef(j_label);
}
}
@@ -365,6 +374,8 @@ void GodotJavaWrapper::end_benchmark_measure(const String &p_context, const Stri
jstring j_context = env->NewStringUTF(p_context.utf8().get_data());
jstring j_label = env->NewStringUTF(p_label.utf8().get_data());
env->CallVoidMethod(godot_instance, _end_benchmark_measure, j_context, j_label);
+ env->DeleteLocalRef(j_context);
+ env->DeleteLocalRef(j_label);
}
}
@@ -374,6 +385,7 @@ void GodotJavaWrapper::dump_benchmark(const String &benchmark_file) {
ERR_FAIL_NULL(env);
jstring j_benchmark_file = env->NewStringUTF(benchmark_file.utf8().get_data());
env->CallVoidMethod(godot_instance, _dump_benchmark, j_benchmark_file);
+ env->DeleteLocalRef(j_benchmark_file);
}
}
@@ -383,7 +395,9 @@ bool GodotJavaWrapper::has_feature(const String &p_feature) const {
ERR_FAIL_NULL_V(env, false);
jstring j_feature = env->NewStringUTF(p_feature.utf8().get_data());
- return env->CallBooleanMethod(godot_instance, _has_feature, j_feature);
+ bool result = env->CallBooleanMethod(godot_instance, _has_feature, j_feature);
+ env->DeleteLocalRef(j_feature);
+ return result;
} else {
return false;
}
diff --git a/platform/android/net_socket_android.cpp b/platform/android/net_socket_android.cpp
index a2befdc9be..8f0ee51fac 100644
--- a/platform/android/net_socket_android.cpp
+++ b/platform/android/net_socket_android.cpp
@@ -49,6 +49,14 @@ void NetSocketAndroid::setup(jobject p_net_utils) {
_multicast_lock_release = env->GetMethodID(cls, "multicastLockRelease", "()V");
}
+void NetSocketAndroid::terminate() {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(cls);
+ env->DeleteGlobalRef(net_utils);
+}
+
void NetSocketAndroid::multicast_lock_acquire() {
if (_multicast_lock_acquire) {
JNIEnv *env = get_jni_env();
diff --git a/platform/android/net_socket_android.h b/platform/android/net_socket_android.h
index e5f46d3236..26cb2d4e3d 100644
--- a/platform/android/net_socket_android.h
+++ b/platform/android/net_socket_android.h
@@ -63,6 +63,7 @@ protected:
public:
static void make_default();
static void setup(jobject p_net_utils);
+ static void terminate();
virtual void close();
diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp
index 82e7fdb320..463a307854 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -162,7 +162,39 @@ Vector<String> OS_Android::get_granted_permissions() const {
return godot_java->get_granted_permissions();
}
-Error OS_Android::open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) {
+bool OS_Android::copy_dynamic_library(const String &p_library_path, const String &p_target_dir, String *r_copy_path) {
+ if (!FileAccess::exists(p_library_path)) {
+ return false;
+ }
+
+ Ref<DirAccess> da_ref = DirAccess::create_for_path(p_library_path);
+ if (!da_ref.is_valid()) {
+ return false;
+ }
+
+ String copy_path = p_target_dir.path_join(p_library_path.get_file());
+ bool copy_exists = FileAccess::exists(copy_path);
+ if (copy_exists) {
+ print_verbose("Deleting existing library copy " + copy_path);
+ if (da_ref->remove(copy_path) != OK) {
+ print_verbose("Unable to delete " + copy_path);
+ }
+ }
+
+ print_verbose("Copying " + p_library_path + " to " + p_target_dir);
+ Error create_dir_result = da_ref->make_dir_recursive(p_target_dir);
+ if (create_dir_result == OK || create_dir_result == ERR_ALREADY_EXISTS) {
+ copy_exists = da_ref->copy(p_library_path, copy_path) == OK;
+ }
+
+ if (copy_exists && r_copy_path != nullptr) {
+ *r_copy_path = copy_path;
+ }
+
+ return copy_exists;
+}
+
+Error OS_Android::open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data) {
String path = p_path;
bool so_file_exists = true;
if (!FileAccess::exists(path)) {
@@ -172,24 +204,32 @@ Error OS_Android::open_dynamic_library(const String &p_path, void *&p_library_ha
p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW);
if (!p_library_handle && so_file_exists) {
- // The library may be on the sdcard and thus inaccessible. Try to copy it to the internal
- // directory.
- uint64_t so_modified_time = FileAccess::get_modified_time(p_path);
- String dynamic_library_path = get_dynamic_libraries_path().path_join(String::num_uint64(so_modified_time));
- String internal_path = dynamic_library_path.path_join(p_path.get_file());
-
- bool internal_so_file_exists = FileAccess::exists(internal_path);
- if (!internal_so_file_exists) {
- Ref<DirAccess> da_ref = DirAccess::create_for_path(p_path);
- if (da_ref.is_valid()) {
- Error create_dir_result = da_ref->make_dir_recursive(dynamic_library_path);
- if (create_dir_result == OK || create_dir_result == ERR_ALREADY_EXISTS) {
- internal_so_file_exists = da_ref->copy(path, internal_path) == OK;
+ // The library (and its dependencies) may be on the sdcard and thus inaccessible.
+ // Try to copy to the internal directory for access.
+ const String dynamic_library_path = get_dynamic_libraries_path();
+
+ if (p_data != nullptr && p_data->library_dependencies != nullptr && !p_data->library_dependencies->is_empty()) {
+ // Copy the library dependencies
+ print_verbose("Copying library dependencies..");
+ for (const String &library_dependency_path : *p_data->library_dependencies) {
+ String internal_library_dependency_path;
+ if (!copy_dynamic_library(library_dependency_path, dynamic_library_path.path_join(library_dependency_path.get_base_dir()), &internal_library_dependency_path)) {
+ ERR_PRINT(vformat("Unable to copy library dependency %s", library_dependency_path));
+ } else {
+ void *lib_dependency_handle = dlopen(internal_library_dependency_path.utf8().get_data(), RTLD_NOW);
+ if (!lib_dependency_handle) {
+ ERR_PRINT(vformat("Can't open dynamic library dependency: %s. Error: %s.", internal_library_dependency_path, dlerror()));
+ }
}
}
}
+ String internal_path;
+ print_verbose("Copying library " + p_path);
+ const bool internal_so_file_exists = copy_dynamic_library(p_path, dynamic_library_path.path_join(p_path.get_base_dir()), &internal_path);
+
if (internal_so_file_exists) {
+ print_verbose("Opening library " + internal_path);
p_library_handle = dlopen(internal_path.utf8().get_data(), RTLD_NOW);
if (p_library_handle) {
path = internal_path;
@@ -199,8 +239,8 @@ Error OS_Android::open_dynamic_library(const String &p_path, void *&p_library_ha
ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, dlerror()));
- if (r_resolved_path != nullptr) {
- *r_resolved_path = path;
+ if (p_data != nullptr && p_data->r_resolved_path != nullptr) {
+ *p_data->r_resolved_path = path;
}
return OK;
@@ -736,6 +776,10 @@ void OS_Android::benchmark_dump() {
}
bool OS_Android::_check_internal_feature_support(const String &p_feature) {
+ if (p_feature == "macos" || p_feature == "web_ios" || p_feature == "web_macos" || p_feature == "windows") {
+ return false;
+ }
+
if (p_feature == "system_fonts") {
return true;
}
diff --git a/platform/android/os_android.h b/platform/android/os_android.h
index 31ee7389df..7bdbeef77a 100644
--- a/platform/android/os_android.h
+++ b/platform/android/os_android.h
@@ -113,7 +113,7 @@ public:
virtual void alert(const String &p_alert, const String &p_title) override;
- virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override;
+ virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data = nullptr) override;
virtual String get_name() const override;
virtual String get_distribution_name() const override;
@@ -178,6 +178,8 @@ public:
private:
// Location where we relocate external dynamic libraries to make them accessible.
String get_dynamic_libraries_path() const;
+ // Copy a dynamic library to the given location to make it accessible for loading.
+ bool copy_dynamic_library(const String &p_library_path, const String &p_target_dir, String *r_copy_path = nullptr);
};
#endif // OS_ANDROID_H
diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp
index 93517d8045..be85e47972 100644
--- a/platform/android/tts_android.cpp
+++ b/platform/android/tts_android.cpp
@@ -77,6 +77,14 @@ void TTS_Android::setup(jobject p_tts) {
}
}
+void TTS_Android::terminate() {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(cls);
+ env->DeleteGlobalRef(tts);
+}
+
void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) {
ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech.");
if (ids.has(p_id)) {
@@ -170,6 +178,8 @@ void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volum
jstring jStrT = env->NewStringUTF(p_text.utf8().get_data());
jstring jStrV = env->NewStringUTF(p_voice.utf8().get_data());
env->CallVoidMethod(tts, _speak, jStrT, jStrV, CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, p_interrupt);
+ env->DeleteLocalRef(jStrT);
+ env->DeleteLocalRef(jStrV);
}
}
diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h
index 39efef6ed1..4cc7c12846 100644
--- a/platform/android/tts_android.h
+++ b/platform/android/tts_android.h
@@ -57,6 +57,7 @@ class TTS_Android {
public:
static void setup(jobject p_tts);
+ static void terminate();
static void _java_utterance_callback(int p_event, int p_id, int p_pos);
static bool is_speaking();