diff options
Diffstat (limited to 'platform/android')
69 files changed, 1883 insertions, 479 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index e4d04f1df9..dfc921cc54 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -21,7 +21,7 @@ android_files = [ "android_keys_utils.cpp", "display_server_android.cpp", "plugin/godot_plugin_jni.cpp", - "vulkan/vulkan_context_android.cpp", + "vulkan_context_android.cpp", ] env_android = env.Clone() @@ -56,7 +56,10 @@ if lib_arch_dir != "": if env.dev_build: lib_type_dir = "dev" elif env.debug_features: - lib_type_dir = "debug" + if env.editor_build and env["store_release"]: + lib_type_dir = "release" + else: + lib_type_dir = "debug" else: # Release lib_type_dir = "release" diff --git a/platform/android/android_input_handler.cpp b/platform/android/android_input_handler.cpp index 63045237e9..8bf5eae2f8 100644 --- a/platform/android/android_input_handler.cpp +++ b/platform/android/android_input_handler.cpp @@ -64,7 +64,7 @@ void AndroidInputHandler::_set_key_modifier_state(Ref<InputEventWithModifiers> e } } -void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicode, int p_key_label, bool p_pressed) { +void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicode, int p_key_label, bool p_pressed, bool p_echo) { static char32_t prev_wc = 0; char32_t unicode = p_unicode; if ((p_unicode & 0xfffffc00) == 0xd800) { @@ -125,6 +125,7 @@ void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicod ev->set_key_label(fix_key_label(p_key_label, keycode)); ev->set_unicode(fix_unicode(unicode)); ev->set_pressed(p_pressed); + ev->set_echo(p_echo); _set_key_modifier_state(ev, keycode); @@ -138,22 +139,19 @@ void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicod } void AndroidInputHandler::_cancel_all_touch() { - _parse_all_touch(false, false, true); + _parse_all_touch(false, true); touch.clear(); } -void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_double_tap, bool reset_index) { +void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_canceled, bool p_double_tap) { if (touch.size()) { //end all if exist for (int i = 0; i < touch.size(); i++) { Ref<InputEventScreenTouch> ev; ev.instantiate(); - if (reset_index) { - ev->set_index(-1); - } else { - ev->set_index(touch[i].id); - } + ev->set_index(touch[i].id); ev->set_pressed(p_pressed); + ev->set_canceled(p_canceled); ev->set_position(touch[i].pos); ev->set_double_tap(p_double_tap); Input::get_singleton()->parse_input_event(ev); @@ -180,7 +178,7 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const } //send touch - _parse_all_touch(true, p_double_tap); + _parse_all_touch(true, false, p_double_tap); } break; case AMOTION_EVENT_ACTION_MOVE: { //motion @@ -257,11 +255,11 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const void AndroidInputHandler::_cancel_mouse_event_info(bool p_source_mouse_relative) { buttons_state = BitField<MouseButtonMask>(); - _parse_mouse_event_info(BitField<MouseButtonMask>(), false, false, p_source_mouse_relative); + _parse_mouse_event_info(BitField<MouseButtonMask>(), false, true, false, p_source_mouse_relative); mouse_event_info.valid = false; } -void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative) { +void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_canceled, bool p_double_click, bool p_source_mouse_relative) { if (!mouse_event_info.valid) { return; } @@ -278,6 +276,7 @@ void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> even hover_prev_pos = mouse_event_info.pos; } ev->set_pressed(p_pressed); + ev->set_canceled(p_canceled); BitField<MouseButtonMask> changed_button_mask = BitField<MouseButtonMask>(buttons_state.operator int64_t() ^ event_buttons_mask.operator int64_t()); buttons_state = event_buttons_mask; @@ -289,7 +288,7 @@ void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> even } void AndroidInputHandler::_release_mouse_event_info(bool p_source_mouse_relative) { - _parse_mouse_event_info(BitField<MouseButtonMask>(), false, false, p_source_mouse_relative); + _parse_mouse_event_info(BitField<MouseButtonMask>(), false, false, false, p_source_mouse_relative); mouse_event_info.valid = false; } @@ -318,7 +317,7 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an mouse_event_info.valid = true; mouse_event_info.pos = p_event_pos; - _parse_mouse_event_info(event_buttons_mask, true, p_double_click, p_source_mouse_relative); + _parse_mouse_event_info(event_buttons_mask, true, false, p_double_click, p_source_mouse_relative); } break; case AMOTION_EVENT_ACTION_CANCEL: { diff --git a/platform/android/android_input_handler.h b/platform/android/android_input_handler.h index 2badd32636..c74c5020e3 100644 --- a/platform/android/android_input_handler.h +++ b/platform/android/android_input_handler.h @@ -83,13 +83,13 @@ private: void _wheel_button_click(BitField<MouseButtonMask> event_buttons_mask, const Ref<InputEventMouseButton> &ev, MouseButton wheel_button, float factor); - void _parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative); + void _parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_canceled, bool p_double_click, bool p_source_mouse_relative); void _release_mouse_event_info(bool p_source_mouse_relative = false); void _cancel_mouse_event_info(bool p_source_mouse_relative = false); - void _parse_all_touch(bool p_pressed, bool p_double_tap, bool reset_index = false); + void _parse_all_touch(bool p_pressed, bool p_canceled = false, bool p_double_tap = false); void _release_all_touch(); @@ -101,7 +101,7 @@ public: void process_magnify(Point2 p_pos, float p_factor); void process_pan(Point2 p_pos, Vector2 p_delta); void process_joy_event(JoypadEvent p_event); - void process_key_event(int p_physical_keycode, int p_unicode, int p_key_label, bool p_pressed); + void process_key_event(int p_physical_keycode, int p_unicode, int p_key_label, bool p_pressed, bool p_echo); }; #endif // ANDROID_INPUT_HANDLER_H diff --git a/platform/android/android_keys_utils.h b/platform/android/android_keys_utils.h index 3a587dd680..5cf5628a8b 100644 --- a/platform/android/android_keys_utils.h +++ b/platform/android/android_keys_utils.h @@ -31,8 +31,9 @@ #ifndef ANDROID_KEYS_UTILS_H #define ANDROID_KEYS_UTILS_H +#include "core/os/keyboard.h" + #include <android/input.h> -#include <core/os/keyboard.h> #define AKEYCODE_MAX 0xFFFF @@ -60,6 +61,7 @@ static AndroidGodotCodePair android_godot_code_pairs[] = { { AKEYCODE_DPAD_DOWN, Key::DOWN }, // (20) Directional Pad Down key. { AKEYCODE_DPAD_LEFT, Key::LEFT }, // (21) Directional Pad Left key. { AKEYCODE_DPAD_RIGHT, Key::RIGHT }, // (22) Directional Pad Right key. + { AKEYCODE_DPAD_CENTER, Key::ENTER }, // (23) Directional Pad Center key. { AKEYCODE_VOLUME_UP, Key::VOLUMEUP }, // (24) Volume Up key. { AKEYCODE_VOLUME_DOWN, Key::VOLUMEDOWN }, // (25) Volume Down key. { AKEYCODE_POWER, Key::STANDBY }, // (26) Power key. diff --git a/platform/android/api/api.cpp b/platform/android/api/api.cpp index 757ca315a7..60c369951c 100644 --- a/platform/android/api/api.cpp +++ b/platform/android/api/api.cpp @@ -30,10 +30,11 @@ #include "api.h" -#include "core/config/engine.h" #include "java_class_wrapper.h" #include "jni_singleton.h" +#include "core/config/engine.h" + #if !defined(ANDROID_ENABLED) static JavaClassWrapper *java_class_wrapper = nullptr; #endif diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h index 455ed259ec..a2d1c08168 100644 --- a/platform/android/api/jni_singleton.h +++ b/platform/android/api/jni_singleton.h @@ -33,8 +33,9 @@ #include "core/config/engine.h" #include "core/variant/variant.h" + #ifdef ANDROID_ENABLED -#include "platform/android/jni_utils.h" +#include "jni_utils.h" #endif class JNISingleton : public Object { diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 5fc32132e3..51e89c720d 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -272,7 +272,8 @@ Error AudioDriverOpenSL::input_start() { return init_input_device(); } - return OK; + WARN_PRINT("Unable to start audio capture - No RECORD_AUDIO permission"); + return ERR_UNAUTHORIZED; } Error AudioDriverOpenSL::input_stop() { diff --git a/platform/android/detect.py b/platform/android/detect.py index ec36a40941..2860898e5c 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -9,10 +9,6 @@ if TYPE_CHECKING: from SCons import Environment -def is_active(): - return True - - def get_name(): return "Android" @@ -22,6 +18,8 @@ def can_build(): def get_opts(): + from SCons.Variables import BoolVariable + return [ ("ANDROID_SDK_ROOT", "Path to the Android SDK", get_env_android_sdk_root()), ( @@ -29,9 +27,20 @@ def get_opts(): 'Target platform (android-<api>, e.g. "android-' + str(get_min_target_api()) + '")', "android-" + str(get_min_target_api()), ), + BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False), ] +def get_doc_classes(): + return [ + "EditorExportPlatformAndroid", + ] + + +def get_doc_path(): + return "doc_classes" + + # Return the ANDROID_SDK_ROOT environment variable. def get_env_android_sdk_root(): return os.environ.get("ANDROID_SDK_ROOT", -1) diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 7b41ad87bd..6bd09fe00a 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -30,10 +30,11 @@ #include "dir_access_jandroid.h" -#include "core/string/print_string.h" #include "string_android.h" #include "thread_jandroid.h" +#include "core/string/print_string.h" + jobject DirAccessJAndroid::dir_access_handler = nullptr; jclass DirAccessJAndroid::cls = nullptr; jmethodID DirAccessJAndroid::_dir_open = nullptr; diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index 05b7d47957..5ee4c85659 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -31,9 +31,11 @@ #ifndef DIR_ACCESS_JANDROID_H #define DIR_ACCESS_JANDROID_H +#include "java_godot_lib_jni.h" + #include "core/io/dir_access.h" #include "drivers/unix/dir_access_unix.h" -#include "java_godot_lib_jni.h" + #include <stdio.h> /// Android implementation of the DirAccess interface used to provide access to diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index af4ba1255b..f02b292868 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -30,19 +30,23 @@ #include "display_server_android.h" -#include "core/config/project_settings.h" #include "java_godot_io_wrapper.h" #include "java_godot_wrapper.h" #include "os_android.h" #include "tts_android.h" +#include "core/config/project_settings.h" + #if defined(VULKAN_ENABLED) +#include "vulkan_context_android.h" + #include "drivers/vulkan/rendering_device_vulkan.h" -#include "platform/android/vulkan/vulkan_context_android.h" #include "servers/rendering/renderer_rd/renderer_compositor_rd.h" #endif + #ifdef GLES3_ENABLED #include "drivers/gles3/rasterizer_gles3.h" + #include <EGL/egl.h> #endif @@ -449,6 +453,10 @@ void DisplayServerAndroid::window_move_to_foreground(DisplayServer::WindowID p_w // Not supported on Android. } +bool DisplayServerAndroid::window_is_focused(WindowID p_window) const { + return true; +} + bool DisplayServerAndroid::window_can_draw(DisplayServer::WindowID p_window) const { return true; } @@ -676,16 +684,19 @@ void DisplayServerAndroid::cursor_set_custom_image(const Ref<Resource> &p_cursor void DisplayServerAndroid::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window) { #if defined(VULKAN_ENABLED) - context_vulkan->set_vsync_mode(p_window, p_vsync_mode); + if (context_vulkan) { + context_vulkan->set_vsync_mode(p_window, p_vsync_mode); + } #endif } DisplayServer::VSyncMode DisplayServerAndroid::window_get_vsync_mode(WindowID p_window) const { #if defined(VULKAN_ENABLED) - return context_vulkan->get_vsync_mode(p_window); -#else - return DisplayServer::VSYNC_ENABLED; + if (context_vulkan) { + return context_vulkan->get_vsync_mode(p_window); + } #endif + return DisplayServer::VSYNC_ENABLED; } void DisplayServerAndroid::reset_swap_buffers_flag() { diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index b2400d81dc..e0ad2cb916 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -39,6 +39,8 @@ class RenderingDeviceVulkan; #endif class DisplayServerAndroid : public DisplayServer { + // No need to register with GDCLASS, it's platform-specific and nothing is added. + String rendering_driver; // https://developer.android.com/reference/android/view/PointerIcon @@ -176,6 +178,7 @@ public: virtual void window_request_attention(WindowID p_window = MAIN_WINDOW_ID) override; virtual void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID) override; + virtual bool window_is_focused(WindowID p_window = MAIN_WINDOW_ID) const override; virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override; diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml new file mode 100644 index 0000000000..6d3affed15 --- /dev/null +++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml @@ -0,0 +1,590 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorExportPlatformAndroid" inherits="EditorExportPlatform" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + Exporter for Android. + </brief_description> + <description> + </description> + <tutorials> + <link title="Exporting for Android">$DOCS_URL/tutorials/export/exporting_for_android.html</link> + <link title="Custom builds for Android">$DOCS_URL/tutorials/export/android_custom_build.html</link> + </tutorials> + <members> + <member name="apk_expansion/SALT" type="String" setter="" getter=""> + 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. + </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". + </member> + <member name="architectures/arm64-v8a" type="bool" setter="" getter=""> + If [code]true[/code], [code]arm64[/code] binaries are included into exported project. + </member> + <member name="architectures/armeabi-v7a" type="bool" setter="" getter=""> + If [code]true[/code], [code]arm32[/code] binaries are included into exported project. + </member> + <member name="architectures/x86" type="bool" setter="" getter=""> + If [code]true[/code], [code]x86_32[/code] binaries are included into exported project. + </member> + <member name="architectures/x86_64" type="bool" setter="" getter=""> + 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. + </member> + <member name="custom_template/debug" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="custom_template/release" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="gradle_build/export_format" type="int" setter="" getter=""> + Export format for Gradle build. + </member> + <member name="gradle_build/min_sdk" type="String" setter="" getter=""> + Minimal Android SDK version for Gradle build. + </member> + <member name="gradle_build/target_sdk" type="String" setter="" getter=""> + Target Android SDK version for Gradle build. + </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. + </member> + <member name="graphics/opengl_debug" type="bool" setter="" getter=""> + If [code]true[/code], OpenGL ES debug context will be created (additional runtime checking, validation, and logging). + </member> + <member name="keystore/debug" type="String" setter="" getter=""> + Path of the debug keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_DEBUG_PATH[/code]. + Fallbacks to [code]EditorSettings.export/android/debug_keystore[/code] if empty. + </member> + <member name="keystore/debug_password" type="String" setter="" getter=""> + Password for the debug keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_DEBUG_PASSWORD[/code]. + Fallbacks to [code]EditorSettings.export/android/debug_keystore_pass[/code] if both it and [member keystore/debug] are empty. + </member> + <member name="keystore/debug_user" type="String" setter="" getter=""> + User name for the debug keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_DEBUG_USER[/code]. + Fallbacks to [code]EditorSettings.export/android/debug_keystore_user[/code] if both it and [member keystore/debug] are empty. + </member> + <member name="keystore/release" type="String" setter="" getter=""> + Path of the release keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_RELEASE_PATH[/code]. + </member> + <member name="keystore/release_password" type="String" setter="" getter=""> + Password for the release keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD[/code]. + </member> + <member name="keystore/release_user" type="String" setter="" getter=""> + User name for the release keystore file. + 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. + </member> + <member name="launcher_icons/adaptive_foreground_432x432" type="String" setter="" getter=""> + Foreground layer of the application adaptive icon file. + </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. + </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. + </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. + </member> + <member name="package/signed" type="bool" setter="" getter=""> + If [code]true[/code], package signing is enabled. + </member> + <member name="package/unique_name" type="String" setter="" getter=""> + Unique application identifier in a reverse-DNS format, can only contain alphanumeric characters ([code]A-Z[/code], [code]a-z[/code], and [code]0-9[/code]), hyphens ([code]-[/code]), and periods ([code].[/code]). + </member> + <member name="permissions/access_checkin_properties" type="bool" setter="" getter=""> + Allows read/write access to the "properties" table in the checkin database. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_CHECKIN_PROPERTIES]ACCESS_CHECKIN_PROPERTIES[/url]. + </member> + <member name="permissions/access_coarse_location" type="bool" setter="" getter=""> + Allows access to the approximate location information. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_COARSE_LOCATION]ACCESS_COARSE_LOCATION[/url]. + </member> + <member name="permissions/access_fine_location" type="bool" setter="" getter=""> + Allows access to the precise location information. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION]ACCESS_FINE_LOCATION[/url]. + </member> + <member name="permissions/access_location_extra_commands" type="bool" setter="" getter=""> + Allows access to the extra location provider commands. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_LOCATION_EXTRA_COMMANDS]ACCESS_LOCATION_EXTRA_COMMANDS[/url]. + </member> + <member name="permissions/access_mock_location" type="bool" setter="" getter=""> + Allows an application to create mock location providers for testing. + </member> + <member name="permissions/access_network_state" type="bool" setter="" getter=""> + Allows access to the information about networks. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_NETWORK_STATE]ACCESS_NETWORK_STATE[/url]. + </member> + <member name="permissions/access_surface_flinger" type="bool" setter="" getter=""> + Allows an application to use SurfaceFlinger's low level features. + </member> + <member name="permissions/access_wifi_state" type="bool" setter="" getter=""> + Allows access to the information about Wi-Fi networks. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_WIFI_STATE]ACCESS_WIFI_STATE[/url]. + </member> + <member name="permissions/account_manager" type="bool" setter="" getter=""> + Allows applications to call into AccountAuthenticators. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCOUNT_MANAGER]ACCOUNT_MANAGER[/url]. + </member> + <member name="permissions/add_voicemail" type="bool" setter="" getter=""> + Allows an application to add voicemails into the system. See [url=https://developer.android.com/reference/android/Manifest.permission#ADD_VOICEMAIL]ADD_VOICEMAIL[/url]. + </member> + <member name="permissions/authenticate_accounts" type="bool" setter="" getter=""> + Allows an application to act as an AccountAuthenticator for the AccountManager. + </member> + <member name="permissions/battery_stats" type="bool" setter="" getter=""> + Allows an application to collect battery statistics. Sett [url=https://developer.android.com/reference/android/Manifest.permission#BATTERY_STATS]BATTERY_STATS[/url]. + </member> + <member name="permissions/bind_accessibility_service" type="bool" setter="" getter=""> + Must be required by an AccessibilityService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_ACCESSIBILITY_SERVICE]BIND_ACCESSIBILITY_SERVICE[/url]. + </member> + <member name="permissions/bind_appwidget" type="bool" setter="" getter=""> + Allows an application to tell the AppWidget service which application can access AppWidget's data. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_APPWIDGET]BIND_APPWIDGET[/url]. + </member> + <member name="permissions/bind_device_admin" type="bool" setter="" getter=""> + Must be required by device administration receiver, to ensure that only the system can interact with it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_DEVICE_ADMIN]BIND_DEVICE_ADMIN[/url]. + </member> + <member name="permissions/bind_input_method" type="bool" setter="" getter=""> + Must be required by an InputMethodService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_INPUT_METHOD]BIND_INPUT_METHOD[/url]. + </member> + <member name="permissions/bind_nfc_service" type="bool" setter="" getter=""> + Must be required by a HostApduService or OffHostApduService to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_NFC_SERVICE]BIND_NFC_SERVICE[/url]. + </member> + <member name="permissions/bind_notification_listener_service" type="bool" setter="" getter=""> + Must be required by a NotificationListenerService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE]BIND_NOTIFICATION_LISTENER_SERVICE[/url]. + </member> + <member name="permissions/bind_print_service" type="bool" setter="" getter=""> + Must be required by a PrintService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_PRINT_SERVICE]BIND_PRINT_SERVICE[/url]. + </member> + <member name="permissions/bind_remoteviews" type="bool" setter="" getter=""> + Must be required by a RemoteViewsService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_REMOTEVIEWS]BIND_REMOTEVIEWS[/url]. + </member> + <member name="permissions/bind_text_service" type="bool" setter="" getter=""> + Must be required by a TextService (e.g. SpellCheckerService) to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_TEXT_SERVICE]BIND_TEXT_SERVICE[/url]. + </member> + <member name="permissions/bind_vpn_service" type="bool" setter="" getter=""> + Must be required by a VpnService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_VPN_SERVICE]BIND_VPN_SERVICE[/url]. + </member> + <member name="permissions/bind_wallpaper" type="bool" setter="" getter=""> + Must be required by a WallpaperService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_WALLPAPER]BIND_WALLPAPER[/url]. + </member> + <member name="permissions/bluetooth" type="bool" setter="" getter=""> + Allows applications to connect to paired bluetooth devices. See [url=https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH]BLUETOOTH[/url]. + </member> + <member name="permissions/bluetooth_admin" type="bool" setter="" getter=""> + Allows applications to discover and pair bluetooth devices. See [url=https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_ADMIN]BLUETOOTH_ADMIN[/url]. + </member> + <member name="permissions/bluetooth_privileged" type="bool" setter="" getter=""> + Allows applications to pair bluetooth devices without user interaction, and to allow or disallow phonebook access or message access. See [url=https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_PRIVILEGED]BLUETOOTH_PRIVILEGED[/url]. + </member> + <member name="permissions/brick" type="bool" setter="" getter=""> + Required to be able to disable the device (very dangerous!). + </member> + <member name="permissions/broadcast_package_removed" type="bool" setter="" getter=""> + Allows an application to broadcast a notification that an application package has been removed. See [url=https://developer.android.com/reference/android/Manifest.permission#BROADCAST_PACKAGE_REMOVED]BROADCAST_PACKAGE_REMOVED[/url]. + </member> + <member name="permissions/broadcast_sms" type="bool" setter="" getter=""> + Allows an application to broadcast an SMS receipt notification. See [url=https://developer.android.com/reference/android/Manifest.permission#BROADCAST_SMS]BROADCAST_SMS[/url]. + </member> + <member name="permissions/broadcast_sticky" type="bool" setter="" getter=""> + Allows an application to broadcast sticky intents. See [url=https://developer.android.com/reference/android/Manifest.permission#BROADCAST_STICKY]BROADCAST_STICKY[/url]. + </member> + <member name="permissions/broadcast_wap_push" type="bool" setter="" getter=""> + Allows an application to broadcast a WAP PUSH receipt notification. See [url=https://developer.android.com/reference/android/Manifest.permission#BROADCAST_WAP_PUSH]BROADCAST_WAP_PUSH[/url]. + </member> + <member name="permissions/call_phone" type="bool" setter="" getter=""> + Allows an application to initiate a phone call without going through the Dialer user interface. See [url=https://developer.android.com/reference/android/Manifest.permission#CALL_PHONE]CALL_PHONE[/url]. + </member> + <member name="permissions/call_privileged" type="bool" setter="" getter=""> + Allows an application to call any phone number, including emergency numbers, without going through the Dialer user interface. See [url=https://developer.android.com/reference/android/Manifest.permission#CALL_PRIVILEGED]CALL_PRIVILEGED[/url]. + </member> + <member name="permissions/camera" type="bool" setter="" getter=""> + Required to be able to access the camera device. See [url=https://developer.android.com/reference/android/Manifest.permission#CAMERA]CAMERA[/url]. + </member> + <member name="permissions/capture_audio_output" type="bool" setter="" getter=""> + Allows an application to capture audio output. See [url=https://developer.android.com/reference/android/Manifest.permission#CAPTURE_AUDIO_OUTPUT]CAPTURE_AUDIO_OUTPUT[/url]. + </member> + <member name="permissions/capture_secure_video_output" type="bool" setter="" getter=""> + Allows an application to capture secure video output. + </member> + <member name="permissions/capture_video_output" type="bool" setter="" getter=""> + Allows an application to capture video output. + </member> + <member name="permissions/change_component_enabled_state" type="bool" setter="" getter=""> + Allows an application to change whether an application component (other than its own) is enabled or not. See [url=https://developer.android.com/reference/android/Manifest.permission#CHANGE_COMPONENT_ENABLED_STATE]CHANGE_COMPONENT_ENABLED_STATE[/url]. + </member> + <member name="permissions/change_configuration" type="bool" setter="" getter=""> + Allows an application to modify the current configuration, such as locale. See [url=https://developer.android.com/reference/android/Manifest.permission#CHANGE_CONFIGURATION]CHANGE_CONFIGURATION[/url]. + </member> + <member name="permissions/change_network_state" type="bool" setter="" getter=""> + Allows applications to change network connectivity state. See [url=https://developer.android.com/reference/android/Manifest.permission#CHANGE_NETWORK_STATE]CHANGE_NETWORK_STATE[/url]. + </member> + <member name="permissions/change_wifi_multicast_state" type="bool" setter="" getter=""> + Allows applications to enter Wi-Fi Multicast mode. See [url=https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_MULTICAST_STATE]CHANGE_WIFI_MULTICAST_STATE[/url]. + </member> + <member name="permissions/change_wifi_state" type="bool" setter="" getter=""> + Allows applications to change Wi-Fi connectivity state. See [url=https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE]CHANGE_WIFI_STATE[/url]. + </member> + <member name="permissions/clear_app_cache" type="bool" setter="" getter=""> + Allows an application to clear the caches of all installed applications on the device. See [url=https://developer.android.com/reference/android/Manifest.permission#CLEAR_APP_CACHE]CLEAR_APP_CACHE[/url]. + </member> + <member name="permissions/clear_app_user_data" type="bool" setter="" getter=""> + Allows an application to clear user data. + </member> + <member name="permissions/control_location_updates" type="bool" setter="" getter=""> + Allows enabling/disabling location update notifications from the radio. See [url=https://developer.android.com/reference/android/Manifest.permission#CONTROL_LOCATION_UPDATES]CONTROL_LOCATION_UPDATES[/url]. + </member> + <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> + <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]. + </member> + <member name="permissions/device_power" type="bool" setter="" getter=""> + Allows low-level access to power management. + </member> + <member name="permissions/diagnostic" type="bool" setter="" getter=""> + Allows applications to RW to diagnostic resources. See [url=https://developer.android.com/reference/android/Manifest.permission#DIAGNOSTIC]DIAGNOSTIC[/url]. + </member> + <member name="permissions/disable_keyguard" type="bool" setter="" getter=""> + Allows applications to disable the keyguard if it is not secure. See [url=https://developer.android.com/reference/android/Manifest.permission#DISABLE_KEYGUARD]DISABLE_KEYGUARD[/url]. + </member> + <member name="permissions/dump" type="bool" setter="" getter=""> + Allows an application to retrieve state dump information from system services. See [url=https://developer.android.com/reference/android/Manifest.permission#DUMP]DUMP[/url]. + </member> + <member name="permissions/expand_status_bar" type="bool" setter="" getter=""> + Allows an application to expand or collapse the status bar. See [url=https://developer.android.com/reference/android/Manifest.permission#EXPAND_STATUS_BAR]EXPAND_STATUS_BAR[/url]. + </member> + <member name="permissions/factory_test" type="bool" setter="" getter=""> + Run as a manufacturer test application, running as the root user. See [url=https://developer.android.com/reference/android/Manifest.permission#FACTORY_TEST]FACTORY_TEST[/url]. + </member> + <member name="permissions/flashlight" type="bool" setter="" getter=""> + Allows access to the flashlight. + </member> + <member name="permissions/force_back" type="bool" setter="" getter=""> + Allows an application to force a BACK operation on whatever is the top activity. + </member> + <member name="permissions/get_accounts" type="bool" setter="" getter=""> + Allows access to the list of accounts in the Accounts Service. See [url=https://developer.android.com/reference/android/Manifest.permission#GET_ACCOUNTS]GET_ACCOUNTS[/url]. + </member> + <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> + <member name="permissions/get_top_activity_info" type="bool" setter="" getter=""> + Allows an application to retrieve private information about the current top activity. + </member> + <member name="permissions/global_search" type="bool" setter="" getter=""> + Used on content providers to allow the global search system to access their data. See [url=https://developer.android.com/reference/android/Manifest.permission#GLOBAL_SEARCH]GLOBAL_SEARCH[/url]. + </member> + <member name="permissions/hardware_test" type="bool" setter="" getter=""> + Allows access to hardware peripherals. + </member> + <member name="permissions/inject_events" type="bool" setter="" getter=""> + Allows an application to inject user events (keys, touch, trackball) into the event stream and deliver them to ANY window. + </member> + <member name="permissions/install_location_provider" type="bool" setter="" getter=""> + Allows an application to install a location provider into the Location Manager. See [url=https://developer.android.com/reference/android/Manifest.permission#INSTALL_LOCATION_PROVIDER]INSTALL_LOCATION_PROVIDER[/url]. + </member> + <member name="permissions/install_packages" type="bool" setter="" getter=""> + Allows an application to install packages. See [url=https://developer.android.com/reference/android/Manifest.permission#INSTALL_PACKAGES]INSTALL_PACKAGES[/url]. + </member> + <member name="permissions/install_shortcut" type="bool" setter="" getter=""> + Allows an application to install a shortcut in Launcher. See [url=https://developer.android.com/reference/android/Manifest.permission#INSTALL_SHORTCUT]INSTALL_SHORTCUT[/url]. + </member> + <member name="permissions/internal_system_window" type="bool" setter="" getter=""> + Allows an application to open windows that are for use by parts of the system user interface. + </member> + <member name="permissions/internet" type="bool" setter="" getter=""> + Allows applications to open network sockets. See [url=https://developer.android.com/reference/android/Manifest.permission#INTERNET]INTERNET[/url]. + </member> + <member name="permissions/kill_background_processes" type="bool" setter="" getter=""> + Allows an application to call ActivityManager.killBackgroundProcesses(String). See [url=https://developer.android.com/reference/android/Manifest.permission#KILL_BACKGROUND_PROCESSES]KILL_BACKGROUND_PROCESSES[/url]. + </member> + <member name="permissions/location_hardware" type="bool" setter="" getter=""> + Allows an application to use location features in hardware, such as the geofencing api. See [url=https://developer.android.com/reference/android/Manifest.permission#LOCATION_HARDWARE]LOCATION_HARDWARE[/url]. + </member> + <member name="permissions/manage_accounts" type="bool" setter="" getter=""> + Allows an application to manage the list of accounts in the AccountManager. + </member> + <member name="permissions/manage_app_tokens" type="bool" setter="" getter=""> + Allows an application to manage (create, destroy, Z-order) application tokens in the window manager. + </member> + <member name="permissions/manage_documents" type="bool" setter="" getter=""> + Allows an application to manage access to documents, usually as part of a document picker. See [url=https://developer.android.com/reference/android/Manifest.permission#MANAGE_DOCUMENTS]MANAGE_DOCUMENTS[/url]. + </member> + <member name="permissions/manage_external_storage" type="bool" setter="" getter=""> + Allows an application a broad access to external storage in scoped storage. See [url=https://developer.android.com/reference/android/Manifest.permission#MANAGE_EXTERNAL_STORAGE]MANAGE_EXTERNAL_STORAGE[/url]. + </member> + <member name="permissions/master_clear" type="bool" setter="" getter=""> + See [url=https://developer.android.com/reference/android/Manifest.permission#MASTER_CLEAR]MASTER_CLEAR[/url]. + </member> + <member name="permissions/media_content_control" type="bool" setter="" getter=""> + Allows an application to know what content is playing and control its playback. See [url=https://developer.android.com/reference/android/Manifest.permission#MEDIA_CONTENT_CONTROL]MEDIA_CONTENT_CONTROL[/url]. + </member> + <member name="permissions/modify_audio_settings" type="bool" setter="" getter=""> + Allows an application to modify global audio settings. See [url=https://developer.android.com/reference/android/Manifest.permission#MODIFY_AUDIO_SETTINGS]MODIFY_AUDIO_SETTINGS[/url]. + </member> + <member name="permissions/modify_phone_state" type="bool" setter="" getter=""> + Allows modification of the telephony state - power on, mmi, etc. Does not include placing calls. See [url=https://developer.android.com/reference/android/Manifest.permission#MODIFY_PHONE_STATE]MODIFY_PHONE_STATE[/url]. + </member> + <member name="permissions/mount_format_filesystems" type="bool" setter="" getter=""> + Allows formatting file systems for removable storage. See [url=https://developer.android.com/reference/android/Manifest.permission#MOUNT_FORMAT_FILESYSTEMS]MOUNT_FORMAT_FILESYSTEMS[/url]. + </member> + <member name="permissions/mount_unmount_filesystems" type="bool" setter="" getter=""> + Allows mounting and unmounting file systems for removable storage. See [url=https://developer.android.com/reference/android/Manifest.permission#MOUNT_UNMOUNT_FILESYSTEMS]MOUNT_UNMOUNT_FILESYSTEMS[/url]. + </member> + <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> + <member name="permissions/process_outgoing_calls" type="bool" setter="" getter=""> + 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]. + </member> + <member name="permissions/read_call_log" type="bool" setter="" getter=""> + Allows an application to read the user's call log. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CALL_LOG]READ_CALL_LOG[/url]. + </member> + <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=""> + 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. + </member> + <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> + <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]. + </member> + <member name="permissions/read_phone_state" type="bool" setter="" getter=""> + Allows read only access to phone state. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE]READ_PHONE_STATE[/url]. + </member> + <member name="permissions/read_profile" type="bool" setter="" getter=""> + Allows an application to read the user's personal profile data. + </member> + <member name="permissions/read_sms" type="bool" setter="" getter=""> + Allows an application to read SMS messages. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_SMS]READ_SMS[/url]. + </member> + <member name="permissions/read_social_stream" type="bool" setter="" getter=""> + Allows an application to read from the user's social stream. + </member> + <member name="permissions/read_sync_settings" type="bool" setter="" getter=""> + Allows applications to read the sync settings. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_SYNC_SETTINGS]READ_SYNC_SETTINGS[/url]. + </member> + <member name="permissions/read_sync_stats" type="bool" setter="" getter=""> + Allows applications to read the sync stats. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_SYNC_STATS]READ_SYNC_STATS[/url]. + </member> + <member name="permissions/read_user_dictionary" type="bool" setter="" getter=""> + Allows an application to read the user dictionary. + </member> + <member name="permissions/reboot" type="bool" setter="" getter=""> + Required to be able to reboot the device. See [url=https://developer.android.com/reference/android/Manifest.permission#REBOOT]REBOOT[/url]. + </member> + <member name="permissions/receive_boot_completed" type="bool" setter="" getter=""> + Allows an application to receive the Intent.ACTION_BOOT_COMPLETED that is broadcast after the system finishes booting. See [url=https://developer.android.com/reference/android/Manifest.permission#RECEIVE_BOOT_COMPLETED]RECEIVE_BOOT_COMPLETED[/url]. + </member> + <member name="permissions/receive_mms" type="bool" setter="" getter=""> + Allows an application to monitor incoming MMS messages. See [url=https://developer.android.com/reference/android/Manifest.permission#RECEIVE_MMS]RECEIVE_MMS[/url]. + </member> + <member name="permissions/receive_sms" type="bool" setter="" getter=""> + Allows an application to receive SMS messages. See [url=https://developer.android.com/reference/android/Manifest.permission#RECEIVE_SMS]RECEIVE_SMS[/url]. + </member> + <member name="permissions/receive_wap_push" type="bool" setter="" getter=""> + Allows an application to receive WAP push messages. See [url=https://developer.android.com/reference/android/Manifest.permission#RECEIVE_WAP_PUSH]RECEIVE_WAP_PUSH[/url]. + </member> + <member name="permissions/record_audio" type="bool" setter="" getter=""> + Allows an application to record audio. See [url=https://developer.android.com/reference/android/Manifest.permission#RECORD_AUDIO]RECORD_AUDIO[/url]. + </member> + <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> + <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]. + </member> + <member name="permissions/send_sms" type="bool" setter="" getter=""> + Allows an application to send SMS messages. See [url=https://developer.android.com/reference/android/Manifest.permission#SEND_SMS]SEND_SMS[/url]. + </member> + <member name="permissions/set_activity_watcher" type="bool" setter="" getter=""> + Allows an application to watch and control how activities are started globally in the system. + </member> + <member name="permissions/set_alarm" type="bool" setter="" getter=""> + Allows an application to broadcast an Intent to set an alarm for the user. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_ALARM]SET_ALARM[/url]. + </member> + <member name="permissions/set_always_finish" type="bool" setter="" getter=""> + Allows an application to control whether activities are immediately finished when put in the background. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_ALWAYS_FINISH]SET_ALWAYS_FINISH[/url]. + </member> + <member name="permissions/set_animation_scale" type="bool" setter="" getter=""> + Allows to modify the global animation scaling factor. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_ANIMATION_SCALE]SET_ANIMATION_SCALE[/url]. + </member> + <member name="permissions/set_debug_app" type="bool" setter="" getter=""> + Configure an application for debugging. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_DEBUG_APP]SET_DEBUG_APP[/url]. + </member> + <member name="permissions/set_orientation" type="bool" setter="" getter=""> + Allows low-level access to setting the orientation (actually rotation) of the screen. + </member> + <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> + <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]. + </member> + <member name="permissions/set_time" type="bool" setter="" getter=""> + Allows applications to set the system time directly. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_TIME]SET_TIME[/url]. + </member> + <member name="permissions/set_time_zone" type="bool" setter="" getter=""> + Allows applications to set the system time zone directly. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_TIME_ZONE]SET_TIME_ZONE[/url]. + </member> + <member name="permissions/set_wallpaper" type="bool" setter="" getter=""> + Allows applications to set the wallpaper. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_WALLPAPER]SET_WALLPAPER[/url]. + </member> + <member name="permissions/set_wallpaper_hints" type="bool" setter="" getter=""> + Allows applications to set the wallpaper hints. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_WALLPAPER_HINTS]SET_WALLPAPER_HINTS[/url]. + </member> + <member name="permissions/signal_persistent_processes" type="bool" setter="" getter=""> + Allow an application to request that a signal be sent to all persistent processes. See [url=https://developer.android.com/reference/android/Manifest.permission#SIGNAL_PERSISTENT_PROCESSES]SIGNAL_PERSISTENT_PROCESSES[/url]. + </member> + <member name="permissions/status_bar" type="bool" setter="" getter=""> + Allows an application to open, close, or disable the status bar and its icons. See [url=https://developer.android.com/reference/android/Manifest.permission#STATUS_BAR]STATUS_BAR[/url]. + </member> + <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> + <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]. + </member> + <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> + <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]. + </member> + <member name="permissions/use_credentials" type="bool" setter="" getter=""> + Allows an application to request authtokens from the AccountManager. + </member> + <member name="permissions/use_sip" type="bool" setter="" getter=""> + Allows an application to use SIP service. See [url=https://developer.android.com/reference/android/Manifest.permission#USE_SIP]USE_SIP[/url]. + </member> + <member name="permissions/vibrate" type="bool" setter="" getter=""> + Allows access to the vibrator. See [url=https://developer.android.com/reference/android/Manifest.permission#VIBRATE]VIBRATE[/url]. + </member> + <member name="permissions/wake_lock" type="bool" setter="" getter=""> + Allows using PowerManager WakeLocks to keep processor from sleeping or screen from dimming. See [url=https://developer.android.com/reference/android/Manifest.permission#WAKE_LOCK]WAKE_LOCK[/url]. + </member> + <member name="permissions/write_apn_settings" type="bool" setter="" getter=""> + Allows applications to write the apn settings and read sensitive fields of an existing apn settings like user and password. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_APN_SETTINGS]WRITE_APN_SETTINGS[/url]. + </member> + <member name="permissions/write_calendar" type="bool" setter="" getter=""> + Allows an application to write the user's calendar data. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_CALENDAR]WRITE_CALENDAR[/url]. + </member> + <member name="permissions/write_call_log" type="bool" setter="" getter=""> + Allows an application to write (but not read) the user's call log data. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_CALL_LOG]WRITE_CALL_LOG[/url]. + </member> + <member name="permissions/write_contacts" type="bool" setter="" getter=""> + Allows an application to write the user's contacts data. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_CONTACTS]WRITE_CONTACTS[/url]. + </member> + <member name="permissions/write_external_storage" type="bool" setter="" getter=""> + Allows an application to write to external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE]WRITE_EXTERNAL_STORAGE[/url]. + </member> + <member name="permissions/write_gservices" type="bool" setter="" getter=""> + Allows an application to modify the Google service map. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_GSERVICES]WRITE_GSERVICES[/url]. + </member> + <member name="permissions/write_history_bookmarks" type="bool" setter="" getter=""> + Allows an application to write (but not read) the user's browsing history and bookmarks. + </member> + <member name="permissions/write_profile" type="bool" setter="" getter=""> + Allows an application to write (but not read) the user's personal profile data. + </member> + <member name="permissions/write_secure_settings" type="bool" setter="" getter=""> + Allows an application to read or write the secure system settings. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_SECURE_SETTINGS]WRITE_SECURE_SETTINGS[/url]. + </member> + <member name="permissions/write_settings" type="bool" setter="" getter=""> + Allows an application to read or write the system settings. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_SETTINGS]WRITE_SETTINGS[/url]. + </member> + <member name="permissions/write_sms" type="bool" setter="" getter=""> + Allows an application to write SMS messages. + </member> + <member name="permissions/write_social_stream" type="bool" setter="" getter=""> + Allows an application to write (but not read) the user's social stream data. + </member> + <member name="permissions/write_sync_settings" type="bool" setter="" getter=""> + Allows applications to write the sync settings. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_SYNC_SETTINGS]WRITE_SYNC_SETTINGS[/url]. + </member> + <member name="permissions/write_user_dictionary" type="bool" setter="" getter=""> + Allows an application to write to the user dictionary. + </member> + <member name="screen/immersive_mode" type="bool" setter="" getter=""> + If [code]true[/code], hides navigation and status bar. + </member> + <member name="screen/support_large" type="bool" setter="" getter=""> + Indicates whether the application supports larger screen form-factors. + </member> + <member name="screen/support_normal" type="bool" setter="" getter=""> + Indicates whether an application supports the "normal" screen form-factors. + </member> + <member name="screen/support_small" type="bool" setter="" getter=""> + Indicates whether the application supports smaller screen form-factors. + </member> + <member name="screen/support_xlarge" type="bool" setter="" getter=""> + Indicates whether the application supports extra large screen form-factors. + </member> + <member name="user_data_backup/allow" type="bool" setter="" getter=""> + If [code]true[/code], allows the application to participate in the backup and restore infrastructure. + </member> + <member name="version/code" type="int" setter="" getter=""> + Machine-readable application version. + </member> + <member name="version/name" type="String" setter="" getter=""> + Application version visible to the user. + </member> + <member name="xr_features/hand_tracking" type="int" setter="" getter=""> + </member> + <member name="xr_features/hand_tracking_frequency" type="int" setter="" getter=""> + </member> + <member name="xr_features/passthrough" type="int" setter="" getter=""> + </member> + <member name="xr_features/xr_mode" type="int" setter="" getter=""> + </member> + </members> +</class> diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index d7fe89d97d..32b46271fd 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -30,10 +30,15 @@ #include "export.h" +#include "export_plugin.h" + #include "core/os/os.h" #include "editor/editor_settings.h" #include "editor/export/editor_export.h" -#include "export_plugin.h" + +void register_android_exporter_types() { + GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformAndroid); +} void register_android_exporter() { #ifndef ANDROID_ENABLED diff --git a/platform/android/export/export.h b/platform/android/export/export.h index 7ada91aec9..bd689a400d 100644 --- a/platform/android/export/export.h +++ b/platform/android/export/export.h @@ -31,6 +31,7 @@ #ifndef ANDROID_EXPORT_H #define ANDROID_EXPORT_H +void register_android_exporter_types(); void register_android_exporter(); #endif // ANDROID_EXPORT_H diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 641258a26c..c94119e13d 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -31,6 +31,8 @@ #include "export_plugin.h" #include "gradle_export_util.h" +#include "logo_svg.gen.h" +#include "run_icon_svg.gen.h" #include "core/config/project_settings.h" #include "core/io/dir_access.h" @@ -46,10 +48,8 @@ #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "main/splash.gen.h" -#include "platform/android/logo_svg.gen.h" -#include "platform/android/run_icon_svg.gen.h" -#include "modules/modules_enabled.gen.h" // For svg. +#include "modules/modules_enabled.gen.h" // For mono and svg. #ifdef MODULE_SVG_ENABLED #include "modules/svg/image_loader_svg.h" #endif @@ -253,7 +253,7 @@ static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/instal static const int OPENGL_MIN_SDK_VERSION = 21; // Should match the value in 'platform/android/java/app/config.gradle#minSdk' static const int VULKAN_MIN_SDK_VERSION = 24; -static const int DEFAULT_TARGET_SDK_VERSION = 32; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' +static const int DEFAULT_TARGET_SDK_VERSION = 33; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' #ifndef ANDROID_ENABLED void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { @@ -1066,8 +1066,13 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p if (_uses_vulkan()) { // Require vulkan hardware level 1 support feature_names.push_back("android.hardware.vulkan.level"); - feature_required_list.push_back(true); + feature_required_list.push_back(false); feature_versions.push_back(1); + + // Require vulkan version 1.0 + feature_names.push_back("android.hardware.vulkan.version"); + feature_required_list.push_back(true); + feature_versions.push_back(0x400003); // Encoded value for api version 1.0 } if (feature_names.size() > 0) { @@ -1698,16 +1703,109 @@ void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPres } } -void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_options) { +String EditorExportPlatformAndroid::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const { + if (p_preset) { + if (p_name == ("apk_expansion/public_key")) { + bool apk_expansion = p_preset->get("apk_expansion/enable"); + String apk_expansion_pkey = p_preset->get("apk_expansion/public_key"); + if (apk_expansion && apk_expansion_pkey.is_empty()) { + return TTR("Invalid public key for APK expansion."); + } + } else if (p_name == "package/unique_name") { + String pn = p_preset->get("package/unique_name"); + String pn_err; + + if (!is_package_name_valid(pn, &pn_err)) { + return TTR("Invalid package name:") + " " + pn_err; + } + } else if (p_name == "gradle_build/use_gradle_build") { + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + String enabled_plugins_names = PluginConfigAndroid::get_plugins_names(get_enabled_plugins(Ref<EditorExportPreset>(p_preset))); + if (!enabled_plugins_names.is_empty() && !gradle_build_enabled) { + return TTR("\"Use Gradle Build\" must be enabled to use the plugins."); + } + } else if (p_name == "xr_features/xr_mode") { + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + int xr_mode_index = p_preset->get("xr_features/xr_mode"); + if (xr_mode_index == XR_MODE_OPENXR && !gradle_build_enabled) { + return TTR("OpenXR requires \"Use Gradle Build\" to be enabled"); + } + } else if (p_name == "xr_features/hand_tracking") { + int xr_mode_index = p_preset->get("xr_features/xr_mode"); + int hand_tracking = p_preset->get("xr_features/hand_tracking"); + if (xr_mode_index != XR_MODE_OPENXR) { + if (hand_tracking > XR_HAND_TRACKING_NONE) { + return TTR("\"Hand Tracking\" is only valid when \"XR Mode\" is \"OpenXR\"."); + } + } + } else if (p_name == "xr_features/passthrough") { + int xr_mode_index = p_preset->get("xr_features/xr_mode"); + int passthrough_mode = p_preset->get("xr_features/passthrough"); + if (xr_mode_index != XR_MODE_OPENXR) { + if (passthrough_mode > XR_PASSTHROUGH_NONE) { + return TTR("\"Passthrough\" is only valid when \"XR Mode\" is \"OpenXR\"."); + } + } + } else if (p_name == "gradle_build/export_format") { + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + if (int(p_preset->get("gradle_build/export_format")) == EXPORT_FORMAT_AAB && !gradle_build_enabled) { + return TTR("\"Export AAB\" is only valid when \"Use Gradle Build\" is enabled."); + } + } else if (p_name == "gradle_build/min_sdk") { + String min_sdk_str = p_preset->get("gradle_build/min_sdk"); + int min_sdk_int = VULKAN_MIN_SDK_VERSION; + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do. + if (!gradle_build_enabled) { + return TTR("\"Min SDK\" can only be overridden when \"Use Gradle Build\" is enabled."); + } + if (!min_sdk_str.is_valid_int()) { + return vformat(TTR("\"Min SDK\" should be a valid integer, but got \"%s\" which is invalid."), min_sdk_str); + } else { + min_sdk_int = min_sdk_str.to_int(); + if (min_sdk_int < OPENGL_MIN_SDK_VERSION) { + return vformat(TTR("\"Min SDK\" cannot be lower than %d, which is the version needed by the Godot library."), OPENGL_MIN_SDK_VERSION); + } + } + } + } else if (p_name == "gradle_build/target_sdk") { + String target_sdk_str = p_preset->get("gradle_build/target_sdk"); + int target_sdk_int = DEFAULT_TARGET_SDK_VERSION; + + String min_sdk_str = p_preset->get("gradle_build/min_sdk"); + int min_sdk_int = VULKAN_MIN_SDK_VERSION; + if (min_sdk_str.is_valid_int()) { + min_sdk_int = min_sdk_str.to_int(); + } + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do. + if (!gradle_build_enabled) { + return TTR("\"Target SDK\" can only be overridden when \"Use Gradle Build\" is enabled."); + } + if (!target_sdk_str.is_valid_int()) { + return vformat(TTR("\"Target SDK\" should be a valid integer, but got \"%s\" which is invalid."), target_sdk_str); + } else { + target_sdk_int = target_sdk_str.to_int(); + if (target_sdk_int < min_sdk_int) { + return TTR("\"Target SDK\" version must be greater or equal to \"Min SDK\" version."); + } + } + } + } + } + return String(); +} + +void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_options) const { r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK, false, true)); // Using String instead of int to default to an empty string (no override) with placeholder for instructions (see GH-62465). // This implies doing validation that the string is a proper int. - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", VULKAN_MIN_SDK_VERSION)), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", VULKAN_MIN_SDK_VERSION)), "", false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "", false, true)); Vector<PluginConfigAndroid> plugins_configs = get_plugins(); for (int i = 0; i < plugins_configs.size(); i++) { @@ -1727,17 +1825,17 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("architectures"), abi)), is_default)); } - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_user"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name"), "1.0")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "org.godotengine.$genname")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "org.godotengine.$genname", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/signed"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "package/app_category", PROPERTY_HINT_ENUM, "Accessibility,Audio,Game,Image,Maps,News,Productivity,Social,Video"), APP_CATEGORY_GAME)); @@ -1750,10 +1848,10 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "graphics/opengl_debug"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/xr_mode", PROPERTY_HINT_ENUM, "Regular,OpenXR"), XR_MODE_REGULAR)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_HAND_TRACKING_NONE)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/xr_mode", PROPERTY_HINT_ENUM, "Regular,OpenXR"), XR_MODE_REGULAR, false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_HAND_TRACKING_NONE, false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking_frequency", PROPERTY_HINT_ENUM, "Low,High"), XR_HAND_TRACKING_FREQUENCY_LOW)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/passthrough", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_PASSTHROUGH_NONE)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/passthrough", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_PASSTHROUGH_NONE, false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/immersive_mode"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_small"), true)); @@ -1765,9 +1863,9 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "command_line/extra_args"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "apk_expansion/enable"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "apk_expansion/enable"), false, false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/SALT"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/public_key", PROPERTY_HINT_MULTILINE_TEXT), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/public_key", PROPERTY_HINT_MULTILINE_TEXT), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "permissions/custom_permissions"), PackedStringArray())); @@ -2129,11 +2227,19 @@ String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_ return apksigner_path; } -bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { +bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const { String err; bool valid = false; const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); +#ifdef MODULE_MONO_ENABLED + err += TTR("Exporting to Android is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target Android with C#/Mono instead.") + "\n"; + err += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n"; + // Don't check for additional errors, as this particular error cannot be resolved. + r_error = err; + return false; +#endif + // Look for export templates (first official, and if defined custom templates). if (!gradle_build_enabled) { @@ -2179,16 +2285,17 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito // Validate the rest of the export configuration. - String dk = p_preset->get("keystore/debug"); - String dk_user = p_preset->get("keystore/debug_user"); - String dk_password = p_preset->get("keystore/debug_password"); + String dk = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + 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); if ((dk.is_empty() || dk_user.is_empty() || dk_password.is_empty()) && (!dk.is_empty() || !dk_user.is_empty() || !dk_password.is_empty())) { valid = false; err += TTR("Either Debug Keystore, Debug User AND Debug Password settings must be configured OR none of them.") + "\n"; } - if (!FileAccess::exists(dk)) { + // Use OR to make the export UI able to show this error. + if ((p_debug || !dk.is_empty()) && !FileAccess::exists(dk)) { dk = EDITOR_GET("export/android/debug_keystore"); if (!FileAccess::exists(dk)) { valid = false; @@ -2196,16 +2303,16 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito } } - String rk = p_preset->get("keystore/release"); - String rk_user = p_preset->get("keystore/release_user"); - String rk_password = p_preset->get("keystore/release_password"); + String rk = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + 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); if ((rk.is_empty() || rk_user.is_empty() || rk_password.is_empty()) && (!rk.is_empty() || !rk_user.is_empty() || !rk_password.is_empty())) { valid = false; err += TTR("Either Release Keystore, Release User AND Release Password settings must be configured OR none of them.") + "\n"; } - if (!rk.is_empty() && !FileAccess::exists(rk)) { + if (!p_debug && !rk.is_empty() && !FileAccess::exists(rk)) { valid = false; err += TTR("Release keystore incorrectly configured in the export preset.") + "\n"; } @@ -2225,7 +2332,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito valid = false; } - // Validate that adb is available + // Validate that adb is available. String adb_path = get_adb_path(); if (!FileAccess::exists(adb_path)) { err += TTR("Unable to find Android SDK platform-tools' adb command."); @@ -2247,7 +2354,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito if (!target_sdk_version.is_valid_int()) { target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION); } - // Validate that apksigner is available + // Validate that apksigner is available. String apksigner_path = get_apksigner_path(target_sdk_version.to_int()); if (!FileAccess::exists(apksigner_path)) { err += TTR("Unable to find Android SDK build-tools' apksigner command."); @@ -2267,111 +2374,39 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const { String err; bool valid = true; - const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); - - // Validate the project configuration. - bool apk_expansion = p_preset->get("apk_expansion/enable"); - if (apk_expansion) { - String apk_expansion_pkey = p_preset->get("apk_expansion/public_key"); - - if (apk_expansion_pkey.is_empty()) { - valid = false; - - err += TTR("Invalid public key for APK expansion.") + "\n"; + List<ExportOption> options; + get_export_options(&options); + for (const EditorExportPlatform::ExportOption &E : options) { + if (get_export_option_visibility(p_preset.ptr(), E.option.name)) { + String warn = get_export_option_warning(p_preset.ptr(), E.option.name); + if (!warn.is_empty()) { + err += warn + "\n"; + if (E.required) { + valid = false; + } + } } } - String pn = p_preset->get("package/unique_name"); - String pn_err; - - if (!is_package_name_valid(pn, &pn_err)) { - valid = false; - err += TTR("Invalid package name:") + " " + pn_err + "\n"; - } - String etc_error = test_etc2(); if (!etc_error.is_empty()) { valid = false; err += etc_error; } - // Ensure that `Use Gradle Build` is enabled if a plugin is selected. - String enabled_plugins_names = PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset)); - if (!enabled_plugins_names.is_empty() && !gradle_build_enabled) { - valid = false; - err += TTR("\"Use Gradle Build\" must be enabled to use the plugins."); - err += "\n"; - } - - // Validate the Xr features are properly populated - int xr_mode_index = p_preset->get("xr_features/xr_mode"); - int hand_tracking = p_preset->get("xr_features/hand_tracking"); - int passthrough_mode = p_preset->get("xr_features/passthrough"); - if (xr_mode_index == XR_MODE_OPENXR && !gradle_build_enabled) { - valid = false; - err += TTR("OpenXR requires \"Use Gradle Build\" to be enabled"); - err += "\n"; - } - - if (xr_mode_index != XR_MODE_OPENXR) { - if (hand_tracking > XR_HAND_TRACKING_NONE) { - valid = false; - err += TTR("\"Hand Tracking\" is only valid when \"XR Mode\" is \"OpenXR\"."); - err += "\n"; - } - - if (passthrough_mode > XR_PASSTHROUGH_NONE) { - valid = false; - err += TTR("\"Passthrough\" is only valid when \"XR Mode\" is \"OpenXR\"."); - err += "\n"; - } - } - - if (int(p_preset->get("gradle_build/export_format")) == EXPORT_FORMAT_AAB && - !gradle_build_enabled) { - valid = false; - err += TTR("\"Export AAB\" is only valid when \"Use Gradle Build\" is enabled."); - err += "\n"; - } - - // Check the min sdk version. String min_sdk_str = p_preset->get("gradle_build/min_sdk"); int min_sdk_int = VULKAN_MIN_SDK_VERSION; if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do. - if (!gradle_build_enabled) { - valid = false; - err += TTR("\"Min SDK\" can only be overridden when \"Use Gradle Build\" is enabled."); - err += "\n"; - } - if (!min_sdk_str.is_valid_int()) { - valid = false; - err += vformat(TTR("\"Min SDK\" should be a valid integer, but got \"%s\" which is invalid."), min_sdk_str); - err += "\n"; - } else { + if (min_sdk_str.is_valid_int()) { min_sdk_int = min_sdk_str.to_int(); - if (min_sdk_int < OPENGL_MIN_SDK_VERSION) { - valid = false; - err += vformat(TTR("\"Min SDK\" cannot be lower than %d, which is the version needed by the Godot library."), OPENGL_MIN_SDK_VERSION); - err += "\n"; - } } } - // Check the target sdk version. String target_sdk_str = p_preset->get("gradle_build/target_sdk"); int target_sdk_int = DEFAULT_TARGET_SDK_VERSION; if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do. - if (!gradle_build_enabled) { - valid = false; - err += TTR("\"Target SDK\" can only be overridden when \"Use Gradle Build\" is enabled."); - err += "\n"; - } - if (!target_sdk_str.is_valid_int()) { - valid = false; - err += vformat(TTR("\"Target SDK\" should be a valid integer, but got \"%s\" which is invalid."), target_sdk_str); - err += "\n"; - } else { + if (target_sdk_str.is_valid_int()) { target_sdk_int = target_sdk_str.to_int(); if (target_sdk_int > DEFAULT_TARGET_SDK_VERSION) { // Warning only, so don't override `valid`. @@ -2381,18 +2416,13 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit } } - if (target_sdk_int < min_sdk_int) { - valid = false; - err += TTR("\"Target SDK\" version must be greater or equal to \"Min SDK\" version."); - err += "\n"; - } - String current_renderer = GLOBAL_GET("rendering/renderer/rendering_method.mobile"); if (current_renderer == "forward_plus") { // Warning only, so don't override `valid`. err += vformat(TTR("The \"%s\" renderer is designed for Desktop devices, and is not suitable for Android devices."), current_renderer); err += "\n"; } + if (_uses_vulkan() && min_sdk_int < VULKAN_MIN_SDK_VERSION) { // Warning only, so don't override `valid`. err += vformat(TTR("\"Min SDK\" should be greater or equal to %d for the \"%s\" renderer."), VULKAN_MIN_SDK_VERSION, current_renderer); @@ -2486,9 +2516,9 @@ 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("keystore/release"); - String release_username = p_preset->get("keystore/release_user"); - String release_password = p_preset->get("keystore/release_password"); + String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + 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"); if (!target_sdk_version.is_valid_int()) { target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION); @@ -2508,9 +2538,9 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre String password; String user; if (p_debug) { - keystore = p_preset->get("keystore/debug"); - password = p_preset->get("keystore/debug_password"); - user = p_preset->get("keystore/debug_user"); + keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + 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); if (keystore.is_empty()) { keystore = EDITOR_GET("export/android/debug_keystore"); @@ -2521,7 +2551,6 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre if (ep.step(vformat(TTR("Signing debug %s..."), export_label), 104)) { return ERR_SKIP; } - } else { keystore = release_keystore; password = release_password; @@ -2770,7 +2799,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP CustomExportData user_data; user_data.assets_directory = assets_directory; user_data.debug = p_debug; - err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); + if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { + err = export_project_files(p_preset, p_debug, ignore_apk_file, &user_data, copy_gradle_so); + } else { + err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); + } if (err != OK) { add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project.")); return err; @@ -2861,9 +2894,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP if (should_sign) { if (p_debug) { - String debug_keystore = p_preset->get("keystore/debug"); - String debug_password = p_preset->get("keystore/debug_password"); - String debug_user = p_preset->get("keystore/debug_user"); + String debug_keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + 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); if (debug_keystore.is_empty()) { debug_keystore = EDITOR_GET("export/android/debug_keystore"); @@ -2883,9 +2916,9 @@ 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("keystore/release"); - String release_username = p_preset->get("keystore/release_user"); - String release_password = p_preset->get("keystore/release_password"); + String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + 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()) { release_keystore = OS::get_singleton()->get_resource_dir().path_join(release_keystore).simplify_path(); } @@ -3261,28 +3294,32 @@ void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref< } EditorExportPlatformAndroid::EditorExportPlatformAndroid() { + if (EditorNode::get_singleton()) { #ifdef MODULE_SVG_ENABLED - Ref<Image> img = memnew(Image); - const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); + Ref<Image> img = memnew(Image); + const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); - ImageLoaderSVG img_loader; - img_loader.create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false); - logo = ImageTexture::create_from_image(img); + ImageLoaderSVG img_loader; + img_loader.create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false); + logo = ImageTexture::create_from_image(img); - img_loader.create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false); - run_icon = ImageTexture::create_from_image(img); + img_loader.create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false); + run_icon = ImageTexture::create_from_image(img); #endif - devices_changed.set(); - plugins_changed.set(); + devices_changed.set(); + plugins_changed.set(); #ifndef ANDROID_ENABLED - check_for_changes_thread.start(_check_for_changes_poll_thread, this); + check_for_changes_thread.start(_check_for_changes_poll_thread, this); #endif + } } EditorExportPlatformAndroid::~EditorExportPlatformAndroid() { #ifndef ANDROID_ENABLED quit_request.set(); - check_for_changes_thread.wait_to_finish(); + if (check_for_changes_thread.is_started()) { + check_for_changes_thread.wait_to_finish(); + } #endif } diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index 337a0228d0..0ac0fbb10b 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -49,6 +49,15 @@ const String SPLASH_CONFIG_XML_CONTENT = R"SPLASH(<?xml version="1.0" encoding=" </layer-list> )SPLASH"; +// Optional environment variables for defining confidential information. If any +// of these is set, they will override the values set in the credentials file. +const String ENV_ANDROID_KEYSTORE_DEBUG_PATH = "GODOT_ANDROID_KEYSTORE_DEBUG_PATH"; +const String ENV_ANDROID_KEYSTORE_DEBUG_USER = "GODOT_ANDROID_KEYSTORE_DEBUG_USER"; +const String ENV_ANDROID_KEYSTORE_DEBUG_PASS = "GODOT_ANDROID_KEYSTORE_DEBUG_PASSWORD"; +const String ENV_ANDROID_KEYSTORE_RELEASE_PATH = "GODOT_ANDROID_KEYSTORE_RELEASE_PATH"; +const String ENV_ANDROID_KEYSTORE_RELEASE_USER = "GODOT_ANDROID_KEYSTORE_RELEASE_USER"; +const String ENV_ANDROID_KEYSTORE_RELEASE_PASS = "GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD"; + struct LauncherIcon { const char *export_path; int dimensions = 0; @@ -72,10 +81,10 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { EditorProgress *ep = nullptr; }; - Vector<PluginConfigAndroid> plugins; + mutable Vector<PluginConfigAndroid> plugins; String last_plugin_names; uint64_t last_gradle_build_time = 0; - SafeFlag plugins_changed; + mutable SafeFlag plugins_changed; Mutex plugins_lock; Vector<Device> devices; SafeFlag devices_changed; @@ -180,7 +189,9 @@ public: public: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; - virtual void get_export_options(List<ExportOption> *r_options) override; + virtual void get_export_options(List<ExportOption> *r_options) const override; + + virtual String get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const override; virtual String get_name() const override; @@ -208,7 +219,7 @@ public: static String get_apksigner_path(int p_target_sdk = -1, bool p_check_executes = false); - virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + 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; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; diff --git a/platform/android/export/godot_plugin_config.cpp b/platform/android/export/godot_plugin_config.cpp index 56431c25de..b64cca3254 100644 --- a/platform/android/export/godot_plugin_config.cpp +++ b/platform/android/export/godot_plugin_config.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "godot_plugin_config.h" + /* * Set of prebuilt plugins. * Currently unused, this is just for future reference: diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index b889d58199..ba4487cc4d 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -275,7 +275,8 @@ String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses } if (p_uses_vulkan) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.level\" android:required=\"true\" android:version=\"1\" />\n"; + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.level\" android:required=\"false\" android:version=\"1\" />\n"; + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.version\" android:required=\"true\" android:version=\"0x400003\" />\n"; } return manifest_xr_features; } @@ -305,6 +306,9 @@ String _get_activity_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_xr " <!-- OpenXR category tag to indicate the activity starts in an immersive OpenXR mode. \n" " See https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#android-runtime-category. -->\n" " <category android:name=\"org.khronos.openxr.intent.category.IMMERSIVE_HMD\" />\n" + "\n" + " <!-- Enable VR access on HTC Vive Focus devices. -->\n" + " <category android:name=\"com.htc.intent.category.VRAPP\" />\n" " </intent-filter>\n"; } else { manifest_activity_text += " <intent-filter>\n" diff --git a/platform/android/logo.svg b/platform/android/export/logo.svg index f154e55d11..f154e55d11 100644 --- a/platform/android/logo.svg +++ b/platform/android/export/logo.svg diff --git a/platform/android/run_icon.svg b/platform/android/export/run_icon.svg index 24d930fece..24d930fece 100644 --- a/platform/android/run_icon.svg +++ b/platform/android/export/run_icon.svg diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index b8f45628e5..7ff5c4d117 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -32,6 +32,7 @@ #define FILE_ACCESS_ANDROID_H #include "core/io/file_access.h" + #include <android/asset_manager.h> #include <android/log.h> #include <stdio.h> diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp index ea8459d1ed..a1865fb1d4 100644 --- a/platform/android/file_access_filesystem_jandroid.cpp +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -30,9 +30,10 @@ #include "file_access_filesystem_jandroid.h" +#include "thread_jandroid.h" + #include "core/os/os.h" #include "core/templates/local_vector.h" -#include "thread_jandroid.h" #include <unistd.h> diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h index 5e74d9de24..739c8a0925 100644 --- a/platform/android/file_access_filesystem_jandroid.h +++ b/platform/android/file_access_filesystem_jandroid.h @@ -31,9 +31,10 @@ #ifndef FILE_ACCESS_FILESYSTEM_JANDROID_H #define FILE_ACCESS_FILESYSTEM_JANDROID_H -#include "core/io/file_access.h" #include "java_godot_lib_jni.h" +#include "core/io/file_access.h" + class FileAccessFilesystemJAndroid : public FileAccess { static jobject file_access_handler; static jclass cls; diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index ce4a2ecfe4..542ab51660 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -35,7 +35,7 @@ android:name=".GodotApp" android:label="@string/godot_project_name_string" android:theme="@style/GodotAppSplashTheme" - android:launchMode="singleTask" + android:launchMode="singleInstance" android:excludeFromRecents="false" android:exported="true" android:screenOrientation="landscape" diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index f1b4bfd534..e7c06628c8 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -1,11 +1,11 @@ ext.versions = [ androidGradlePlugin: '7.2.1', - compileSdk : 32, - // Also update 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION' + compileSdk : 33, + // Also update 'platform/android/export/export_plugin.cpp#OPENGL_MIN_SDK_VERSION' minSdk : 21, // Also update 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION' - targetSdk : 32, - buildTools : '32.0.0', + targetSdk : 33, + buildTools : '33.0.2', kotlinVersion : '1.7.0', fragmentVersion : '1.3.6', nexusPublishVersion: '1.1.0', @@ -135,14 +135,16 @@ ext.generateGodotLibraryVersion = { List<String> requiredKeys -> String statusValue = map["status"] if (statusValue == null) { statusCode = 0 - } else if (statusValue.startsWith("alpha")) { + } else if (statusValue.startsWith("dev")) { statusCode = 1 - } else if (statusValue.startsWith("beta")) { + } else if (statusValue.startsWith("alpha")) { statusCode = 2 - } else if (statusValue.startsWith("rc")) { + } else if (statusValue.startsWith("beta")) { statusCode = 3 - } else if (statusValue.startsWith("stable")) { + } else if (statusValue.startsWith("rc")) { statusCode = 4 + } else if (statusValue.startsWith("stable")) { + statusCode = 5 } else { statusCode = 0 } @@ -189,6 +191,9 @@ ext.getGodotPublishVersion = { -> String versionName = "" int versionCode = 1 (versionName, versionCode) = generateGodotLibraryVersion(requiredKeys) + if (!versionName.endsWith("stable")) { + versionName += "-SNAPSHOT" + } return versionName } diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index cffe0a33d9..f94454e2a7 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -9,7 +9,7 @@ buildscript { dependencies { classpath libraries.androidGradlePlugin classpath libraries.kotlinGradlePlugin - classpath 'io.github.gradle-nexus:publish-plugin:1.1.0' + classpath 'io.github.gradle-nexus:publish-plugin:1.3.0' } } @@ -20,6 +20,13 @@ plugins { apply from: 'app/config.gradle' apply from: 'scripts/publish-root.gradle' +ext { + PUBLISH_VERSION = getGodotPublishVersion() +} + +group = ossrhGroupId +version = PUBLISH_VERSION + allprojects { repositories { google() @@ -31,9 +38,7 @@ ext { supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"] supportedFlavors = ["editor", "template"] supportedFlavorsBuildTypes = [ - // The editor can't be used with target=release as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - "editor": ["dev", "debug"], + "editor": ["dev", "debug", "release"], "template": ["dev", "debug", "release"] ] @@ -47,6 +52,7 @@ ext { def rootDir = "../../.." def binDir = "$rootDir/bin/" +def androidEditorBuildsDir = "$binDir/android_editor_builds/" def getSconsTaskName(String flavor, String buildType, String abi) { return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize() @@ -214,18 +220,46 @@ def isAndroidStudio() { return sysProps != null && sysProps['idea.platform.prefix'] != null } -task copyEditorDebugBinaryToBin(type: Copy) { +task copyEditorReleaseApkToBin(type: Copy) { + dependsOn ':editor:assembleRelease' + from('editor/build/outputs/apk/release') + into(androidEditorBuildsDir) + include('android_editor-release*.apk') +} + +task copyEditorReleaseAabToBin(type: Copy) { + dependsOn ':editor:bundleRelease' + from('editor/build/outputs/bundle/release') + into(androidEditorBuildsDir) + include('android_editor-release*.aab') +} + +task copyEditorDebugApkToBin(type: Copy) { dependsOn ':editor:assembleDebug' from('editor/build/outputs/apk/debug') - into(binDir) - include('android_editor.apk') + into(androidEditorBuildsDir) + include('android_editor-debug.apk') +} + +task copyEditorDebugAabToBin(type: Copy) { + dependsOn ':editor:bundleDebug' + from('editor/build/outputs/bundle/debug') + into(androidEditorBuildsDir) + include('android_editor-debug.aab') } -task copyEditorDevBinaryToBin(type: Copy) { +task copyEditorDevApkToBin(type: Copy) { dependsOn ':editor:assembleDev' from('editor/build/outputs/apk/dev') - into(binDir) - include('android_editor_dev.apk') + into(androidEditorBuildsDir) + include('android_editor-dev.apk') +} + +task copyEditorDevAabToBin(type: Copy) { + dependsOn ':editor:bundleDev' + from('editor/build/outputs/bundle/dev') + into(androidEditorBuildsDir) + include('android_editor-dev.aab') } /** @@ -246,7 +280,8 @@ task generateGodotEditor { && targetLibs.isDirectory() && targetLibs.listFiles() != null && targetLibs.listFiles().length > 0) { - tasks += "copyEditor${target.capitalize()}BinaryToBin" + tasks += "copyEditor${target.capitalize()}ApkToBin" + tasks += "copyEditor${target.capitalize()}AabToBin" } } @@ -294,9 +329,11 @@ task cleanGodotEditor(type: Delete) { // Delete the generated binary apks delete("editor/build/outputs/apk") - // Delete the Godot editor apks in the Godot bin directory - delete("$binDir/android_editor.apk") - delete("$binDir/android_editor_dev.apk") + // Delete the generated aab binaries + delete("editor/build/outputs/bundle") + + // Delete the Godot editor apks & aabs in the Godot bin directory + delete(androidEditorBuildsDir) } /** diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 9152492e9d..38034aa47c 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -13,22 +13,67 @@ dependencies { } ext { - // Build number added as a suffix to the version code, and incremented for each build/upload to - // the Google Play store. - // This should be reset on each stable release of Godot. - editorBuildNumber = 0 + // Retrieve the build number from the environment variable; default to 0 if none is specified. + // The build number is added as a suffix to the version code for upload to the Google Play store. + getEditorBuildNumber = { -> + int buildNumber = 0 + String versionStatus = System.getenv("GODOT_VERSION_STATUS") + if (versionStatus != null && !versionStatus.isEmpty()) { + try { + buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", "")); + } catch (NumberFormatException ignored) { + buildNumber = 0 + } + } + + return buildNumber + } // Value by which the Godot version code should be offset by to make room for the build number editorBuildNumberOffset = 100 + + // Return the keystore file used for signing the release build. + getGodotKeystoreFile = { -> + def keyStore = System.getenv("GODOT_ANDROID_SIGN_KEYSTORE") + if (keyStore == null) { + return null + } + return file(keyStore) + } + + // Return the key alias used for signing the release build. + getGodotKeyAlias = { -> + def kAlias = System.getenv("GODOT_ANDROID_KEYSTORE_ALIAS") + return kAlias + } + + // Return the password for the key used for signing the release build. + getGodotSigningPassword = { -> + def signingPassword = System.getenv("GODOT_ANDROID_SIGN_PASSWORD") + return signingPassword + } + + // Returns true if the environment variables contains the configuration for signing the release + // build. + hasReleaseSigningConfigs = { -> + def keystoreFile = getGodotKeystoreFile() + def keyAlias = getGodotKeyAlias() + def signingPassword = getGodotSigningPassword() + + return keystoreFile != null && keystoreFile.isFile() + && keyAlias != null && !keyAlias.isEmpty() + && signingPassword != null && !signingPassword.isEmpty() + } } def generateVersionCode() { int libraryVersionCode = getGodotLibraryVersionCode() - return (libraryVersionCode * editorBuildNumberOffset) + editorBuildNumber + return (libraryVersionCode * editorBuildNumberOffset) + getEditorBuildNumber() } def generateVersionName() { String libraryVersionName = getGodotLibraryVersionName() - return libraryVersionName + ".$editorBuildNumber" + int buildNumber = getEditorBuildNumber() + return buildNumber == 0 ? libraryVersionName : libraryVersionName + ".$buildNumber" } android { @@ -45,6 +90,7 @@ android { targetSdkVersion versions.targetSdk missingDimensionStrategy 'products', 'editor' + setProperty("archivesBaseName", "android_editor") } compileOptions { @@ -56,6 +102,15 @@ android { jvmTarget = versions.javaVersion } + signingConfigs { + release { + storeFile getGodotKeystoreFile() + storePassword getGodotSigningPassword() + keyAlias getGodotKeyAlias() + keyPassword getGodotSigningPassword() + } + } + buildTypes { dev { initWith debug @@ -64,15 +119,14 @@ android { debug { initWith release - - // Need to swap with the release signing config when this is ready for public release. + applicationIdSuffix ".debug" signingConfig signingConfigs.debug } release { - // This buildtype is disabled below. - // The editor can't be used with target=release only, as debugging tools are then not - // included, and it would crash on errors instead of reporting them. + if (hasReleaseSigningConfigs()) { + signingConfig signingConfigs.release + } } } @@ -82,20 +136,4 @@ android { doNotStrip '**/*.so' } } - - // Disable 'release' buildtype. - // The editor can't be used with target=release only, as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - variantFilter { variant -> - if (variant.buildType.name == "release") { - setIgnore(true) - } - } - - applicationVariants.all { variant -> - variant.outputs.all { output -> - def suffix = variant.name == "dev" ? "_dev" : "" - output.outputFileName = "android_editor${suffix}.apk" - } - } } diff --git a/platform/android/java/editor/src/debug/res/values/strings.xml b/platform/android/java/editor/src/debug/res/values/strings.xml new file mode 100644 index 0000000000..09ee2d77e1 --- /dev/null +++ b/platform/android/java/editor/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_editor_name_string">Godot Editor 4 (debug)</string> +</resources> diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index 80ef10b6a4..c7b2c8ad67 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -21,6 +21,8 @@ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29"/> <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.RECORD_AUDIO" /> + <uses-permission android:name="android.permission.VIBRATE" /> <application android:allowBackup="false" @@ -33,7 +35,7 @@ <activity android:name=".GodotProjectManager" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" - android:launchMode="singleTask" + android:launchMode="singleInstance" android:screenOrientation="userLandscape" android:exported="true" android:process=":GodotProjectManager"> @@ -51,7 +53,7 @@ android:name=".GodotEditor" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:process=":GodotEditor" - android:launchMode="singleTask" + android:launchMode="singleInstance" android:screenOrientation="userLandscape" android:exported="false"> <layout android:defaultHeight="@dimen/editor_default_window_height" @@ -63,7 +65,7 @@ android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:label="@string/godot_project_name_string" android:process=":GodotGame" - android:launchMode="singleTask" + android:launchMode="singleInstance" android:exported="false" android:screenOrientation="userLandscape"> <layout android:defaultHeight="@dimen/editor_default_window_height" 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 71385315ae..64d3d4eca1 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 @@ -40,6 +40,7 @@ import android.util.Log import android.widget.Toast import androidx.window.layout.WindowMetricsCalculator import org.godotengine.godot.FullScreenGodotApp +import org.godotengine.godot.GodotLib import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.ProcessPhoenix import java.util.* @@ -61,7 +62,7 @@ open class GodotEditor : FullScreenGodotApp() { private const val WAIT_FOR_DEBUGGER = false - private const val COMMAND_LINE_PARAMS = "command_line_params" + private const val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" private const val EDITOR_ID = 777 private const val EDITOR_ARG = "--editor" @@ -75,14 +76,27 @@ open class GodotEditor : FullScreenGodotApp() { private const val PROJECT_MANAGER_ARG = "--project-manager" private const val PROJECT_MANAGER_ARG_SHORT = "-p" private const val PROJECT_MANAGER_PROCESS_NAME_SUFFIX = ":GodotProjectManager" + + /** + * Sets of constants to specify the window to use to run the project. + * + * Should match the values in 'editor/editor_settings.cpp' for the + * 'run/window_placement/android_window' setting. + */ + private const val ANDROID_WINDOW_AUTO = 0 + private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1 + private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2 } private val commandLineParams = ArrayList<String>() override fun onCreate(savedInstanceState: Bundle?) { - PermissionsUtil.requestManifestPermissions(this) + // We exclude certain permissions from the set we request at startup, as they'll be + // requested on demand based on use-cases. + PermissionsUtil.requestManifestPermissions(this, setOf(Manifest.permission.RECORD_AUDIO)) - val params = intent.getStringArrayExtra(COMMAND_LINE_PARAMS) + val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS) + Log.d(TAG, "Received parameters ${params.contentToString()}") updateCommandLineParams(params) if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) { @@ -90,20 +104,44 @@ open class GodotEditor : FullScreenGodotApp() { } super.onCreate(savedInstanceState) + } + + override fun onGodotSetupCompleted() { + super.onGodotSetupCompleted() + val longPressEnabled = enableLongPressGestures() + val panScaleEnabled = enablePanAndScaleGestures() + + checkForProjectPermissionsToEnable() - // Enable long press, panning and scaling gestures - godotFragment?.renderView?.inputHandler?.apply { - enableLongPress(enableLongPressGestures()) - enablePanningAndScalingGestures(enablePanAndScaleGestures()) + runOnUiThread { + // Enable long press, panning and scaling gestures + godotFragment?.renderView?.inputHandler?.apply { + enableLongPress(longPressEnabled) + enablePanningAndScalingGestures(panScaleEnabled) + } + } + } + + /** + * Check for project permissions to enable + */ + protected open fun checkForProjectPermissionsToEnable() { + // Check for RECORD_AUDIO permission + val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input")); + if (audioInputEnabled) { + PermissionsUtil.requestPermission(Manifest.permission.RECORD_AUDIO, this) } } private fun updateCommandLineParams(args: Array<String>?) { // Update the list of command line params with the new args commandLineParams.clear() - if (args != null && args.isNotEmpty()) { + if (!args.isNullOrEmpty()) { commandLineParams.addAll(listOf(*args)) } + if (BuildConfig.BUILD_TYPE == "dev") { + commandLineParams.add("--benchmark") + } } override fun getCommandLine() = commandLineParams @@ -115,8 +153,7 @@ open class GodotEditor : FullScreenGodotApp() { // Whether we should launch the new godot instance in an adjacent window // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT - var launchAdjacent = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && (isInMultiWindowMode || isLargeScreen) + var launchAdjacent = shouldGameLaunchAdjacent() for (arg in args) { if (EDITOR_ARG == arg || EDITOR_ARG_SHORT == arg) { @@ -137,33 +174,49 @@ open class GodotEditor : FullScreenGodotApp() { // Launch a new activity val newInstance = Intent(this, targetClass) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(COMMAND_LINE_PARAMS, args) + .putExtra(EXTRA_COMMAND_LINE_PARAMS, args) if (launchAdjacent) { newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) } if (targetClass == javaClass) { - Log.d(TAG, "Restarting $targetClass") + Log.d(TAG, "Restarting $targetClass with parameters ${args.contentToString()}") ProcessPhoenix.triggerRebirth(this, newInstance) } else { - Log.d(TAG, "Starting $targetClass") + Log.d(TAG, "Starting $targetClass with parameters ${args.contentToString()}") + newInstance.putExtra(EXTRA_NEW_LAUNCH, true) startActivity(newInstance) } return instanceId } override fun onGodotForceQuit(godotInstanceId: Int): Boolean { - val processNameSuffix = when (godotInstanceId) { + val targetClass: Class<*>? + val processNameSuffix: String + when (godotInstanceId) { GAME_ID -> { - GAME_PROCESS_NAME_SUFFIX + processNameSuffix = GAME_PROCESS_NAME_SUFFIX + targetClass = GodotGame::class.java } EDITOR_ID -> { - EDITOR_PROCESS_NAME_SUFFIX + processNameSuffix = EDITOR_PROCESS_NAME_SUFFIX + targetClass = GodotEditor::class.java } PROJECT_MANAGER_ID -> { - PROJECT_MANAGER_PROCESS_NAME_SUFFIX + processNameSuffix = PROJECT_MANAGER_PROCESS_NAME_SUFFIX + targetClass = GodotProjectManager::class.java } - else -> "" + else -> { + processNameSuffix = "" + targetClass = null + } + } + + if (targetClass == javaClass) { + Log.d(TAG, "Force quitting $targetClass") + ProcessPhoenix.forceQuit(this) + return true } + if (processNameSuffix.isBlank()) { return false } @@ -172,8 +225,16 @@ open class GodotEditor : FullScreenGodotApp() { val runningProcesses = activityManager.runningAppProcesses for (runningProcess in runningProcesses) { if (runningProcess.processName.endsWith(processNameSuffix)) { - Log.v(TAG, "Killing Godot process ${runningProcess.processName}") - Process.killProcess(runningProcess.pid) + if (targetClass == null) { + // Killing process directly + Log.v(TAG, "Killing Godot process ${runningProcess.processName}") + Process.killProcess(runningProcess.pid) + } else { + // Activity is running; sending a request for self termination. + Log.v(TAG, "Sending force quit request to $targetClass running on process ${runningProcess.processName}") + val forceQuitIntent = Intent(this, targetClass).putExtra(EXTRA_FORCE_QUIT, true) + startActivity(forceQuitIntent) + } return true } } @@ -210,12 +271,34 @@ open class GodotEditor : FullScreenGodotApp() { /** * Enable long press gestures for the Godot Android editor. */ - protected open fun enableLongPressGestures() = true + protected open fun enableLongPressGestures() = + java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_long_press_as_right_click")) /** * Enable pan and scale gestures for the Godot Android editor. */ - protected open fun enablePanAndScaleGestures() = true + protected open fun enablePanAndScaleGestures() = + java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures")) + + private fun shouldGameLaunchAdjacent(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + try { + when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) { + ANDROID_WINDOW_SAME_AS_EDITOR -> false + ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> true + else -> { + // ANDROID_WINDOW_AUTO + isInMultiWindowMode || isLargeScreen + } + } + } catch (e: NumberFormatException) { + // Fall-back to the 'Auto' behavior + isInMultiWindowMode || isLargeScreen + } + } else { + false + } + } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 104af0fcff..aa4d02b5b2 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -39,4 +39,9 @@ class GodotGame : GodotEditor() { override fun enableLongPressGestures() = false override fun enablePanAndScaleGestures() = false + + override fun checkForProjectPermissionsToEnable() { + // Nothing to do.. by the time we get here, the project permissions will have already + // been requested by the Editor window. + } } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt index 62a9384f2e..d0e4279eeb 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt @@ -37,4 +37,9 @@ package org.godotengine.editor * Upon selection of a project, this activity (via its parent logic) starts the * [GodotEditor] activity. */ -class GodotProjectManager : GodotEditor() +class GodotProjectManager : GodotEditor() { + override fun checkForProjectPermissionsToEnable() { + // Nothing to do here.. we have yet to select a project to load. + } +} + diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties index 41dfb87909..aa991fceae 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.properties +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 841656a240..4340250ad3 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -4,7 +4,6 @@ plugins { } ext { - PUBLISH_VERSION = getGodotPublishVersion() PUBLISH_ARTIFACT_ID = 'godot' } @@ -81,19 +80,11 @@ android { release.jniLibs.srcDirs = ['libs/release'] // Editor jni library + editorRelease.jniLibs.srcDirs = ['libs/tools/release'] editorDebug.jniLibs.srcDirs = ['libs/tools/debug'] editorDev.jniLibs.srcDirs = ['libs/tools/dev'] } - // Disable 'editorRelease'. - // The editor can't be used with target=release as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - variantFilter { variant -> - if (variant.name == "editorRelease") { - setIgnore(true) - } - } - libraryVariants.all { variant -> def flavorName = variant.getFlavorName() if (flavorName == null || flavorName == "") { @@ -106,9 +97,14 @@ android { } boolean devBuild = buildType == "dev" + boolean runTests = devBuild + boolean productionBuild = !devBuild + boolean storeRelease = buildType == "release" def sconsTarget = flavorName if (sconsTarget == "template") { + // Tests are not supported on template builds + runTests = false switch (buildType) { case "release": sconsTarget += "_release" @@ -136,10 +132,10 @@ android { def sconsExts = (org.gradle.internal.os.OperatingSystem.current().isWindows() ? [".bat", ".cmd", ".ps1", ".exe"] : [""]) - logger.lifecycle("Looking for $sconsName executable path") + logger.debug("Looking for $sconsName executable path") for (ext in sconsExts) { String sconsNameExt = sconsName + ext - logger.lifecycle("Checking $sconsNameExt") + logger.debug("Checking $sconsNameExt") sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt) if (sconsExecutableFile != null) { // We're done! @@ -156,7 +152,7 @@ android { if (sconsExecutableFile == null) { throw new GradleException("Unable to find executable path for the '$sconsName' command.") } else { - logger.lifecycle("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}") + logger.debug("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}") } for (String selectedAbi : selectedAbis) { @@ -168,7 +164,7 @@ android { def taskName = getSconsTaskName(flavorName, buildType, selectedAbi) tasks.create(name: taskName, type: Exec) { executable sconsExecutableFile.absolutePath - args "--directory=${pathToRootDir}", "platform=android", "dev_mode=${devBuild}", "dev_build=${devBuild}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() + args "--directory=${pathToRootDir}", "platform=android", "store_release=${storeRelease}", "production=${productionBuild}", "dev_mode=${devBuild}", "dev_build=${devBuild}", "tests=${runTests}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() } // Schedule the tasks so the generated libs are present before the aar file is packaged. diff --git a/platform/android/java/lib/res/xml/godot_provider_paths.xml b/platform/android/java/lib/res/xml/godot_provider_paths.xml index 1255f576bf..8ad16da879 100644 --- a/platform/android/java/lib/res/xml/godot_provider_paths.xml +++ b/platform/android/java/lib/res/xml/godot_provider_paths.xml @@ -1,6 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> + <files-path + name="filesRoot" + path="/" /> + <external-path name="public" path="." /> diff --git a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java index 677c9d8f13..3e975449d8 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -51,6 +51,9 @@ import androidx.fragment.app.FragmentActivity; public abstract class FullScreenGodotApp extends FragmentActivity implements GodotHost { private static final String TAG = FullScreenGodotApp.class.getSimpleName(); + protected static final String EXTRA_FORCE_QUIT = "force_quit_requested"; + protected static final String EXTRA_NEW_LAUNCH = "new_launch_requested"; + @Nullable private Godot godotFragment; @@ -59,6 +62,8 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God super.onCreate(savedInstanceState); setContentView(R.layout.godot_app_layout); + handleStartIntent(getIntent(), true); + Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.godot_fragment_container); if (currentFragment instanceof Godot) { Log.v(TAG, "Reusing existing Godot fragment instance."); @@ -109,11 +114,35 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God @Override public void onNewIntent(Intent intent) { super.onNewIntent(intent); + setIntent(intent); + + handleStartIntent(intent, false); + if (godotFragment != null) { godotFragment.onNewIntent(intent); } } + private void handleStartIntent(Intent intent, boolean newLaunch) { + boolean forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false); + if (forceQuitRequested) { + Log.d(TAG, "Force quit requested, terminating.."); + ProcessPhoenix.forceQuit(this); + return; + } + + if (!newLaunch) { + boolean newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false); + if (newLaunchRequested) { + Log.d(TAG, "New launch requested, restarting.."); + + Intent restartIntent = new Intent(intent).putExtra(EXTRA_NEW_LAUNCH, false); + ProcessPhoenix.triggerRebirth(this, restartIntent); + return; + } + } + } + @CallSuper @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java index a03da7292b..9f2dec7317 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -39,6 +39,7 @@ import org.godotengine.godot.io.file.FileAccessHandler; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; import org.godotengine.godot.tts.GodotTTS; +import org.godotengine.godot.utils.BenchmarkUtils; import org.godotengine.godot.utils.GodotNetUtils; import org.godotengine.godot.utils.PermissionsUtil; import org.godotengine.godot.xr.XRMode; @@ -74,10 +75,14 @@ import android.util.Log; import android.view.Display; import android.view.LayoutInflater; import android.view.Surface; +import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.view.WindowManager; import android.widget.Button; import android.widget.FrameLayout; @@ -176,7 +181,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC public GodotIO io; public GodotNetUtils netUtils; public GodotTTS tts; - DirectoryAccessHandler directoryAccessHandler; + private DirectoryAccessHandler directoryAccessHandler; + private FileAccessHandler fileAccessHandler; public interface ResultCallback { void callback(int requestCode, int resultCode, Intent data); @@ -270,7 +276,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC // ...add to FrameLayout containerLayout.addView(editText); - if (!GodotLib.setup(command_line)) { + tts = new GodotTTS(activity); + + if (!GodotLib.setup(command_line, tts)) { Log.e(TAG, "Unable to setup the Godot engine! Aborting..."); alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit); return false; @@ -278,7 +286,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC if (usesVulkan()) { if (!meetsVulkanRequirements(activity.getPackageManager())) { - Log.w(TAG, "Missing requirements for vulkan support!"); + alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit); + return false; } mRenderView = new GodotVulkanRenderView(activity, this); } else { @@ -291,14 +300,64 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC editText.setView(mRenderView); io.setEdit(editText); - view.getViewTreeObserver().addOnGlobalLayoutListener(() -> { - Point fullSize = new Point(); - activity.getWindowManager().getDefaultDisplay().getSize(fullSize); - Rect gameSize = new Rect(); - mRenderView.getView().getWindowVisibleDisplayFrame(gameSize); - final int keyboardHeight = fullSize.y - gameSize.bottom; - GodotLib.setVirtualKeyboardHeight(keyboardHeight); - }); + // Listeners for keyboard height. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Report the height of virtual keyboard as it changes during the animation. + final View decorView = activity.getWindow().getDecorView(); + decorView.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP) { + int startBottom, endBottom; + @Override + public void onPrepare(@NonNull WindowInsetsAnimation animation) { + startBottom = decorView.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom; + } + + @NonNull + @Override + public WindowInsetsAnimation.Bounds onStart(@NonNull WindowInsetsAnimation animation, @NonNull WindowInsetsAnimation.Bounds bounds) { + endBottom = decorView.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom; + return bounds; + } + + @NonNull + @Override + public WindowInsets onProgress(@NonNull WindowInsets windowInsets, @NonNull List<WindowInsetsAnimation> list) { + // Find the IME animation. + WindowInsetsAnimation imeAnimation = null; + for (WindowInsetsAnimation animation : list) { + if ((animation.getTypeMask() & WindowInsets.Type.ime()) != 0) { + imeAnimation = animation; + break; + } + } + // Update keyboard height based on IME animation. + if (imeAnimation != null) { + float interpolatedFraction = imeAnimation.getInterpolatedFraction(); + // Linear interpolation between start and end values. + float keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction; + GodotLib.setVirtualKeyboardHeight((int)keyboardHeight); + } + return windowInsets; + } + + @Override + public void onEnd(@NonNull WindowInsetsAnimation animation) { + } + }); + } else { + // Infer the virtual keyboard height using visible area. + view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + // Don't allocate a new Rect every time the callback is called. + final Rect visibleSize = new Rect(); + + @Override + public void onGlobalLayout() { + final SurfaceView view = mRenderView.getView(); + view.getWindowVisibleDisplayFrame(visibleSize); + final int keyboardHeight = view.getHeight() - visibleSize.bottom; + GodotLib.setVirtualKeyboardHeight(keyboardHeight); + } + }); + } mRenderView.queueOnRenderThread(() -> { for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { @@ -338,7 +397,17 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC return false; } - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) { + // Optional requirements.. log as warning if missing + Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1"); + } + + // Check for api version 1.0 + return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003); + } + + return false; } public void setKeepScreenOn(final boolean p_enabled) { @@ -455,7 +524,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } return cmdline; } catch (Exception e) { - e.printStackTrace(); + // The _cl_ file can be missing with no adverse effect return new String[0]; } } @@ -509,10 +578,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final Activity activity = getActivity(); io = new GodotIO(activity); netUtils = new GodotNetUtils(activity); - tts = new GodotTTS(activity); Context context = getContext(); directoryAccessHandler = new DirectoryAccessHandler(context); - FileAccessHandler fileAccessHandler = new FileAccessHandler(context); + fileAccessHandler = new FileAccessHandler(context); mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); @@ -526,8 +594,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC netUtils, directoryAccessHandler, fileAccessHandler, - use_apk_expansion, - tts); + use_apk_expansion); result_callback = null; } @@ -540,6 +607,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC @Override public void onCreate(Bundle icicle) { + BenchmarkUtils.beginBenchmarkMeasure("Godot::onCreate"); super.onCreate(icicle); final Activity activity = getActivity(); @@ -588,6 +656,18 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC editor.apply(); i++; + } else if (command_line[i].equals("--benchmark")) { + BenchmarkUtils.setUseBenchmark(true); + new_args.add(command_line[i]); + } else if (has_extra && command_line[i].equals("--benchmark-file")) { + BenchmarkUtils.setUseBenchmark(true); + new_args.add(command_line[i]); + + // Retrieve the filepath + BenchmarkUtils.setBenchmarkFile(command_line[i + 1]); + new_args.add(command_line[i + 1]); + + i++; } else if (command_line[i].trim().length() != 0) { new_args.add(command_line[i]); } @@ -631,8 +711,14 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC Intent notifierIntent = new Intent(activity, activity.getClass()); notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, - notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + pendingIntent = PendingIntent.getActivity(activity, 0, + notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } else { + pendingIntent = PendingIntent.getActivity(activity, 0, + notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } int startResult; try { @@ -658,6 +744,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC mCurrentIntent = activity.getIntent(); initializeGodot(); + BenchmarkUtils.endBenchmarkMeasure("Godot::onCreate"); } @Override @@ -863,20 +950,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC // Do something here if sensor accuracy changes. } - /* - @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getKeyCode()==KeyEvent.KEYCODE_BACK) { - System.out.printf("** BACK REQUEST!\n"); - - GodotLib.quit(); - return true; - } - System.out.printf("** OTHER KEY!\n"); - - return false; - } - */ - public void onBackPressed() { boolean shouldQuit = true; @@ -979,6 +1052,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC return PermissionsUtil.getGrantedPermissions(getActivity()); } + @Keep + private String getCACertificates() { + return GodotNetUtils.getCACertificates(); + } + /** * The download state should trigger changes in the UI --- it may be useful * to show the state as being indeterminate at times. This sample can be @@ -1083,10 +1161,35 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } @Keep + public DirectoryAccessHandler getDirectoryAccessHandler() { + return directoryAccessHandler; + } + + @Keep + public FileAccessHandler getFileAccessHandler() { + return fileAccessHandler; + } + + @Keep private int createNewGodotInstance(String[] args) { if (godotHost != null) { return godotHost.onNewGodotInstanceRequested(args); } return 0; } + + @Keep + private void beginBenchmarkMeasure(String label) { + BenchmarkUtils.beginBenchmarkMeasure(label); + } + + @Keep + private void endBenchmarkMeasure(String label) { + BenchmarkUtils.endBenchmarkMeasure(label); + } + + @Keep + private void dumpBenchmark(String benchmarkFile) { + BenchmarkUtils.dumpBenchmark(fileAccessHandler, benchmarkFile); + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index 330e2ede76..b465377743 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -166,8 +166,10 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView @Override public void requestPointerCapture() { - super.requestPointerCapture(); - inputHandler.onPointerCaptureChange(true); + if (canCapturePointer()) { + super.requestPointerCapture(); + inputHandler.onPointerCaptureChange(true); + } } @Override @@ -188,10 +190,10 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView try { Bitmap bitmap = null; if (!TextUtils.isEmpty(imagePath)) { - if (godot.directoryAccessHandler.filesystemFileExists(imagePath)) { + if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) { // Try to load the bitmap from the file system bitmap = BitmapFactory.decodeFile(imagePath); - } else if (godot.directoryAccessHandler.assetsFileExists(imagePath)) { + } else if (godot.getDirectoryAccessHandler().assetsFileExists(imagePath)) { // Try to load the bitmap from the assets directory AssetManager am = getContext().getAssets(); InputStream imageInputStream = am.open(imagePath); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index 75a01dc787..b9ecd6971d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -61,8 +61,7 @@ public class GodotLib { GodotNetUtils netUtils, DirectoryAccessHandler directoryAccessHandler, FileAccessHandler fileAccessHandler, - boolean use_apk_expansion, - GodotTTS tts); + boolean use_apk_expansion); /** * Invoked on the main thread to clean up Godot native layer. @@ -74,7 +73,7 @@ public class GodotLib { * Invoked on the GL thread to complete setup for the Godot native layer logic. * @param p_cmdline Command line arguments used to configure Godot native layer components. */ - public static native boolean setup(String[] p_cmdline); + public static native boolean setup(String[] p_cmdline, GodotTTS tts); /** * Invoked on the GL thread when the underlying Android surface has changed size. @@ -148,7 +147,7 @@ public class GodotLib { /** * Forward regular key events. */ - public static native void key(int p_physical_keycode, int p_unicode, int p_key_label, boolean p_pressed); + public static native void key(int p_physical_keycode, int p_unicode, int p_key_label, boolean p_pressed, boolean p_echo); /** * Forward game device's key events. @@ -190,6 +189,13 @@ public class GodotLib { public static native String getGlobal(String p_key); /** + * Used to access Godot's editor settings. + * @param settingKey Setting key + * @return String value of the setting + */ + public static native String getEditorSetting(String settingKey); + + /** * Invoke method |p_method| on the Godot object specified by |p_id| * @param p_id Id of the Godot object to invoke * @param p_method Name of the method to invoke diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java index 02c0d67fff..00243dab2a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -51,4 +51,8 @@ public interface GodotRenderView { void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY); void setPointerIcon(int pointerType); + + default boolean canCapturePointer() { + return getInputHandler().canCapturePointer(); + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index 34490d4625..681e182adb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -134,8 +134,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV @Override public void requestPointerCapture() { - super.requestPointerCapture(); - mInputHandler.onPointerCaptureChange(true); + if (canCapturePointer()) { + super.requestPointerCapture(); + mInputHandler.onPointerCaptureChange(true); + } } @Override @@ -162,10 +164,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV try { Bitmap bitmap = null; if (!TextUtils.isEmpty(imagePath)) { - if (godot.directoryAccessHandler.filesystemFileExists(imagePath)) { + if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) { // Try to load the bitmap from the file system bitmap = BitmapFactory.decodeFile(imagePath); - } else if (godot.directoryAccessHandler.assetsFileExists(imagePath)) { + } else if (godot.getDirectoryAccessHandler().assetsFileExists(imagePath)) { // Try to load the bitmap from the assets directory AssetManager am = getContext().getAssets(); InputStream imageInputStream = am.open(imagePath); diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt index 1be009b6dc..e26c9d39c2 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt @@ -197,7 +197,10 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (event.actionMasked == MotionEvent.ACTION_UP) { nextDownIsDoubleTap = false GodotInputHandler.handleMotionEvent(event) + } else if (event.actionMasked == MotionEvent.ACTION_MOVE && panningAndScalingEnabled == false) { + GodotInputHandler.handleMotionEvent(event) } + return true } @@ -224,42 +227,43 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi ) dragInProgress = false } - return true } - dragInProgress = true - val x = terminusEvent.x val y = terminusEvent.y - if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress) { + if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress && !dragInProgress) { GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f) - } else { + } else if (!scaleInProgress){ + dragInProgress = true GodotInputHandler.handleMotionEvent(terminusEvent) } return true } - override fun onScale(detector: ScaleGestureDetector?): Boolean { - if (detector == null || !panningAndScalingEnabled || pointerCaptureInProgress) { + override fun onScale(detector: ScaleGestureDetector): Boolean { + if (!panningAndScalingEnabled || pointerCaptureInProgress || dragInProgress) { return false } - GodotLib.magnify( - detector.focusX, - detector.focusY, - detector.scaleFactor - ) + + if (detector.scaleFactor >= 0.8f && detector.scaleFactor != 1f && detector.scaleFactor <= 1.2f) { + GodotLib.magnify( + detector.focusX, + detector.focusY, + detector.scaleFactor + ) + } return true } - override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean { - if (detector == null || !panningAndScalingEnabled || pointerCaptureInProgress) { + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + if (!panningAndScalingEnabled || pointerCaptureInProgress || dragInProgress) { return false } scaleInProgress = true return true } - override fun onScaleEnd(detector: ScaleGestureDetector?) { + override fun onScaleEnd(detector: ScaleGestureDetector) { scaleInProgress = false } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index cedbbfb7c3..185d03fe39 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -66,6 +66,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { private final ScaleGestureDetector scaleGestureDetector; private final GodotGestureHandler godotGestureHandler; + /** + * Used to decide whether mouse capture can be enabled. + */ + private int lastSeenToolType = MotionEvent.TOOL_TYPE_UNKNOWN; + public GodotInputHandler(GodotRenderView godotView) { final Context context = godotView.getView().getContext(); mRenderView = godotView; @@ -105,6 +110,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD; } + public boolean canCapturePointer() { + return lastSeenToolType == MotionEvent.TOOL_TYPE_MOUSE; + } + public void onPointerCaptureChange(boolean hasCapture) { godotGestureHandler.onPointerCaptureChange(hasCapture); } @@ -132,7 +141,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); final int key_label = event.getDisplayLabel(); - GodotLib.key(physical_keycode, unicode, key_label, false); + GodotLib.key(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0); }; return true; @@ -167,13 +176,15 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); final int key_label = event.getDisplayLabel(); - GodotLib.key(physical_keycode, unicode, key_label, true); + GodotLib.key(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0); } return true; } public boolean onTouchEvent(final MotionEvent event) { + lastSeenToolType = event.getToolType(0); + this.scaleGestureDetector.onTouchEvent(event); if (this.gestureDetector.onTouchEvent(event)) { // The gesture detector has handled the event. @@ -198,6 +209,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } public boolean onGenericMotionEvent(MotionEvent event) { + lastSeenToolType = event.getToolType(0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && gestureDetector.onGenericMotionEvent(event)) { // The gesture detector has handled the event. return true; @@ -471,15 +484,27 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative) { + // Fix the buttonsMask + switch (eventAction) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + // Zero-up the button state + buttonsMask = 0; + break; + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + if (buttonsMask == 0) { + buttonsMask = MotionEvent.BUTTON_PRIMARY; + } + break; + } + // We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically // follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate // stream of events to the engine. switch (eventAction) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: - // Zero-up the button state - buttonsMask = 0; - // FALL THROUGH case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_EXIT: diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index 7b628e25ed..06b565c30f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -93,8 +93,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene @Override public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) { for (int i = 0; i < count; ++i) { - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, true); - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, false); + GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, true, false); + GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, false, false); if (mHasSelection) { mHasSelection = false; @@ -115,8 +115,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene // Return keys are handled through action events continue; } - GodotLib.key(0, character, 0, true); - GodotLib.key(0, character, 0, false); + GodotLib.key(0, character, 0, true, false); + GodotLib.key(0, character, 0, false, false); } } @@ -124,19 +124,20 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene public boolean onEditorAction(final TextView pTextView, final int pActionID, final KeyEvent pKeyEvent) { if (mEdit == pTextView && isFullScreenEdit() && pKeyEvent != null) { final String characters = pKeyEvent.getCharacters(); - - for (int i = 0; i < characters.length(); i++) { - final int character = characters.codePointAt(i); - GodotLib.key(0, character, 0, true); - GodotLib.key(0, character, 0, false); + if (characters != null) { + for (int i = 0; i < characters.length(); i++) { + final int character = characters.codePointAt(i); + GodotLib.key(0, character, 0, true, false); + GodotLib.key(0, character, 0, false, false); + } } } if (pActionID == EditorInfo.IME_ACTION_DONE) { // Enter key has been pressed mRenderView.queueOnRenderThread(() -> { - GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, true); - GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, false); + GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, true, false); + GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, false, false); }); mRenderView.getView().requestFocus(); return true; diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt index 833ab40af0..8ee3d5f48f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt @@ -76,6 +76,13 @@ internal enum class StorageScope { return UNKNOWN } + // If we have 'All Files Access' permission, we can access all directories without + // restriction. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && Environment.isExternalStorageManager()) { + return APP + } + val canonicalPathFile = pathFile.canonicalPath if (internalAppDir != null && canonicalPathFile.startsWith(internalAppDir)) { @@ -90,7 +97,7 @@ internal enum class StorageScope { return APP } - var rootDir: String? = System.getenv("ANDROID_ROOT") + val rootDir: String? = System.getenv("ANDROID_ROOT") if (rootDir != null && canonicalPathFile.startsWith(rootDir)) { return APP } 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 357008ca66..984bf607d0 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 @@ -46,7 +46,7 @@ class FileAccessHandler(val context: Context) { private val TAG = FileAccessHandler::class.java.simpleName private const val FILE_NOT_FOUND_ERROR_ID = -1 - private const val INVALID_FILE_ID = 0 + internal const val INVALID_FILE_ID = 0 private const val STARTING_FILE_ID = 1 internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean { @@ -96,13 +96,17 @@ class FileAccessHandler(val context: Context) { private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0 fun fileOpen(path: String?, modeFlags: Int): Int { + val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID + return fileOpen(path, accessFlag) + } + + internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int { val storageScope = storageScopeIdentifier.identifyStorageScope(path) if (storageScope == StorageScope.UNKNOWN) { return INVALID_FILE_ID } try { - val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID files.put(++lastFileId, dataAccess) diff --git a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java index ebab8398de..edace53e7f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java @@ -62,8 +62,9 @@ public class GodotTTS extends UtteranceProgressListener { final private static int EVENT_CANCEL = 2; final private static int EVENT_BOUNDARY = 3; - final private TextToSpeech synth; - final private LinkedList<GodotUtterance> queue; + final private Activity activity; + private TextToSpeech synth; + private LinkedList<GodotUtterance> queue; final private Object lock = new Object(); private GodotUtterance lastUtterance; @@ -71,10 +72,7 @@ public class GodotTTS extends UtteranceProgressListener { private boolean paused; public GodotTTS(Activity p_activity) { - synth = new TextToSpeech(p_activity, null); - queue = new LinkedList<GodotUtterance>(); - - synth.setOnUtteranceProgressListener(this); + activity = p_activity; } private void updateTTS() { @@ -187,6 +185,16 @@ public class GodotTTS extends UtteranceProgressListener { } /** + * Initialize synth and query. + */ + public void init() { + synth = new TextToSpeech(activity, null); + queue = new LinkedList<GodotUtterance>(); + + synth.setOnUtteranceProgressListener(this); + } + + /** * Adds an utterance to the queue. */ public void speak(String text, String voice, int volume, float pitch, float rate, int utterance_id, boolean interrupt) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt new file mode 100644 index 0000000000..1552c8f082 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt @@ -0,0 +1,122 @@ +/**************************************************************************/ +/* BenchmarkUtils.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. */ +/**************************************************************************/ + +@file:JvmName("BenchmarkUtils") + +package org.godotengine.godot.utils + +import android.os.Build +import android.os.SystemClock +import android.os.Trace +import android.util.Log +import org.godotengine.godot.BuildConfig +import org.godotengine.godot.io.file.FileAccessFlags +import org.godotengine.godot.io.file.FileAccessHandler +import org.json.JSONObject +import java.nio.ByteBuffer +import java.util.concurrent.ConcurrentSkipListMap + +/** + * Contains benchmark related utilities methods + */ +private const val TAG = "GodotBenchmark" + +var useBenchmark = false +var benchmarkFile = "" + +private val startBenchmarkFrom = ConcurrentSkipListMap<String, Long>() +private val benchmarkTracker = ConcurrentSkipListMap<String, Double>() + +/** + * Start measuring and tracing the execution of a given section of code using the given label. + * + * Must be followed by a call to [endBenchmarkMeasure]. + * + * Note: Only enabled on 'editorDev' build variant. + */ +fun beginBenchmarkMeasure(label: String) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + startBenchmarkFrom[label] = SystemClock.elapsedRealtime() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Trace.beginAsyncSection(label, 0) + } +} + +/** + * End measuring and tracing of the section of code with the given label. + * + * Must be preceded by a call [beginBenchmarkMeasure] + * + * * Note: Only enabled on 'editorDev' build variant. + */ +fun endBenchmarkMeasure(label: String) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + val startTime = startBenchmarkFrom[label] ?: return + val total = SystemClock.elapsedRealtime() - startTime + benchmarkTracker[label] = total / 1000.0 + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Trace.endAsyncSection(label, 0) + } +} + +/** + * Dump the benchmark measurements. + * If [filepath] is valid, the data is also written in json format to the specified file. + * + * * Note: Only enabled on 'editorDev' build variant. + */ +@JvmOverloads +fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = benchmarkFile) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + if (!useBenchmark) { + return + } + + val printOut = + benchmarkTracker.map { "\t- ${it.key} : ${it.value} sec." }.joinToString("\n") + Log.i(TAG, "BENCHMARK:\n$printOut") + + if (fileAccessHandler != null && !filepath.isNullOrBlank()) { + val fileId = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE) + if (fileId != FileAccessHandler.INVALID_FILE_ID) { + val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4) + fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray())) + fileAccessHandler.fileClose(fileId) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java b/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java index 401c105cd7..c31d56a3e1 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java @@ -33,11 +33,17 @@ package org.godotengine.godot.utils; import android.app.Activity; import android.content.Context; import android.net.wifi.WifiManager; +import android.util.Base64; import android.util.Log; +import java.io.StringWriter; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Enumeration; + /** * This class handles Android-specific networking functions. - * For now, it only provides access to WifiManager.MulticastLock, which is needed on some devices + * It provides access to the CA certificates KeyStore, and the WifiManager.MulticastLock, which is needed on some devices * to receive broadcast and multicast packets. */ public class GodotNetUtils { @@ -79,4 +85,34 @@ public class GodotNetUtils { Log.e("Godot", "Exception during multicast lock release: " + e); } } + + /** + * Retrieves the list of trusted CA certificates from the "AndroidCAStore" and returns them in PRM format. + * @see https://developer.android.com/reference/java/security/KeyStore . + * @return A string of concatenated X509 certificates in PEM format. + */ + public static String getCACertificates() { + try { + KeyStore ks = KeyStore.getInstance("AndroidCAStore"); + StringBuilder writer = new StringBuilder(); + + if (ks != null) { + ks.load(null, null); + Enumeration<String> aliases = ks.aliases(); + + while (aliases.hasMoreElements()) { + String alias = (String)aliases.nextElement(); + + X509Certificate cert = (X509Certificate)ks.getCertificate(alias); + writer.append("-----BEGIN CERTIFICATE-----\n"); + writer.append(Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT)); + writer.append("-----END CERTIFICATE-----\n"); + } + } + return writer.toString(); + } catch (Exception e) { + Log.e("Godot", "Exception while reading CA certificates: " + e); + return ""; + } + } } 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 e34c94975b..a94188c405 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 @@ -42,10 +42,12 @@ import android.os.Environment; import android.provider.Settings; import android.util.Log; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * This class includes utility functions for Android permissions related operations. @@ -58,6 +60,7 @@ public final class PermissionsUtil { static final int REQUEST_CAMERA_PERMISSION = 2; static final int REQUEST_VIBRATE_PERMISSION = 3; public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001; + public static final int REQUEST_SINGLE_PERMISSION_REQ_CODE = 1002; public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002; private PermissionsUtil() { @@ -65,31 +68,57 @@ 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 name the name of the requested permission. + * @param permissionName the name of the requested permission. * @param activity the caller activity for this method. * @return true/false. "true" if permission was granted otherwise returns "false". */ - public static boolean requestPermission(String name, Activity activity) { + public static boolean requestPermission(String permissionName, Activity activity) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Not necessary, asked on install already return true; } - if (name.equals("RECORD_AUDIO") && ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO_PERMISSION); - return false; - } + switch (permissionName) { + case "RECORD_AUDIO": + case Manifest.permission.RECORD_AUDIO: + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO_PERMISSION); + return false; + } + return true; - if (name.equals("CAMERA") && ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] { Manifest.permission.CAMERA }, REQUEST_CAMERA_PERMISSION); - return false; - } + case "CAMERA": + case Manifest.permission.CAMERA: + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { Manifest.permission.CAMERA }, REQUEST_CAMERA_PERMISSION); + return false; + } + return true; - if (name.equals("VIBRATE") && ContextCompat.checkSelfPermission(activity, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] { Manifest.permission.VIBRATE }, REQUEST_VIBRATE_PERMISSION); - return false; + case "VIBRATE": + case Manifest.permission.VIBRATE: + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { Manifest.permission.VIBRATE }, REQUEST_VIBRATE_PERMISSION); + return false; + } + return true; + + default: + // Check if the given permission is a dangerous permission + try { + PermissionInfo permissionInfo = getPermissionInfo(activity, permissionName); + int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permissionName) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { permissionName }, REQUEST_SINGLE_PERMISSION_REQ_CODE); + return false; + } + } catch (PackageManager.NameNotFoundException e) { + // Unknown permission - return false as it can't be granted. + Log.w(TAG, "Unable to identify permission " + permissionName, e); + return false; + } + return true; } - return true; } /** @@ -98,6 +127,16 @@ public final class PermissionsUtil { * @return true/false. "true" if all permissions were granted otherwise returns "false". */ public static boolean requestManifestPermissions(Activity activity) { + return requestManifestPermissions(activity, null); + } + + /** + * Request dangerous permissions which are defined in the Android manifest file from the user. + * @param activity the caller activity for this method. + * @param excludes Set of permissions to exclude from the request + * @return true/false. "true" if all permissions were granted otherwise returns "false". + */ + public static boolean requestManifestPermissions(Activity activity, @Nullable Set<String> excludes) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } @@ -115,6 +154,9 @@ public final class PermissionsUtil { 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()) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java index 3ee3478fcb..b1bce45fbb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java @@ -90,7 +90,7 @@ public final class ProcessPhoenix extends Activity { */ public static void forceQuit(Activity activity, int pid) { Process.killProcess(pid); // Kill original main process - activity.finish(); + activity.finishAndRemoveTask(); Runtime.getRuntime().exit(0); // Kill kill kill! } diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index 99e29bb53d..c113a13040 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -31,12 +31,13 @@ #ifndef JAVA_GODOT_IO_WRAPPER_H #define JAVA_GODOT_IO_WRAPPER_H -#include <android/log.h> -#include <jni.h> +#include "string_android.h" #include "core/math/rect2i.h" #include "core/variant/typed_array.h" -#include "string_android.h" + +#include <android/log.h> +#include <jni.h> // Class that makes functions in java/src/org/godotengine/godot/GodotIO.java callable from C++ class GodotIOJavaWrapper { diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index e7abe580f1..b54491e0e1 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -30,32 +30,35 @@ #include "java_godot_lib_jni.h" -#include "java_godot_io_wrapper.h" -#include "java_godot_wrapper.h" - -#include "android/asset_manager_jni.h" #include "android_input_handler.h" #include "api/java_class_wrapper.h" #include "api/jni_singleton.h" -#include "core/config/engine.h" -#include "core/config/project_settings.h" -#include "core/input/input.h" #include "dir_access_jandroid.h" #include "display_server_android.h" #include "file_access_android.h" #include "file_access_filesystem_jandroid.h" +#include "java_godot_io_wrapper.h" +#include "java_godot_wrapper.h" #include "jni_utils.h" -#include "main/main.h" #include "net_socket_android.h" #include "os_android.h" #include "string_android.h" #include "thread_jandroid.h" #include "tts_android.h" -#include <android/input.h> -#include <unistd.h> +#include "core/config/engine.h" +#include "core/config/project_settings.h" +#include "core/input/input.h" +#include "main/main.h" + +#ifdef TOOLS_ENABLED +#include "editor/editor_settings.h" +#endif +#include <android/asset_manager_jni.h> +#include <android/input.h> #include <android/native_window_jni.h> +#include <unistd.h> static JavaClassWrapper *java_class_wrapper = nullptr; static OS_Android *os_android = nullptr; @@ -112,7 +115,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei } } -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion, jobject p_godot_tts) { +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion) { JavaVM *jvm; env->GetJavaVM(&jvm); @@ -129,7 +132,6 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv DirAccessJAndroid::setup(p_directory_access_handler); FileAccessFilesystemJAndroid::setup(p_file_access_handler); NetSocketAndroid::setup(p_net_utils); - TTS_Android::setup(p_godot_tts); os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion); @@ -140,7 +142,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env _terminate(env, false); } -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline) { +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline, jobject p_godot_tts) { setup_android_thread(); const char **cmdline = nullptr; @@ -181,6 +183,8 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env return false; } + TTS_Android::setup(p_godot_tts); + java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity())); GDREGISTER_CLASS(JNISingleton); return true; @@ -195,10 +199,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, j if (p_surface) { ANativeWindow *native_window = ANativeWindow_fromSurface(env, p_surface); os_android->set_native_window(native_window); - - DisplayServerAndroid::get_singleton()->reset_window(); - DisplayServerAndroid::get_singleton()->notify_surface_changed(p_width, p_height); } + DisplayServerAndroid::get_singleton()->reset_window(); + DisplayServerAndroid::get_singleton()->notify_surface_changed(p_width, p_height); } } } @@ -240,7 +243,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, if (step.get() == 0) { // Since Godot is initialized on the UI thread, main_thread_id was set to that thread's id, // but for Godot purposes, the main thread is the one running the game loop - Main::setup2(Thread::get_caller_id()); + Main::setup2(); input_handler = new AndroidInputHandler(); step.increment(); return true; @@ -382,11 +385,11 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged( } // Called on the UI thread -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed, jboolean p_echo) { if (step.get() <= 0) { return; } - input_handler->process_key_event(p_physical_keycode, p_unicode, p_key_label, p_pressed); + input_handler->process_key_event(p_physical_keycode, p_unicode, p_key_label, p_pressed, p_echo); } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z) { @@ -427,43 +430,45 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv * return env->NewStringUTF(GLOBAL_GET(js).operator String().utf8().get_data()); } +JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getEditorSetting(JNIEnv *env, jclass clazz, jstring p_setting_key) { + String editor_setting = ""; +#ifdef TOOLS_ENABLED + String godot_setting_key = jstring_to_string(p_setting_key, env); + editor_setting = EDITOR_GET(godot_setting_key).operator String(); +#else + WARN_PRINT("Access to the Editor Settings in only available on Editor builds"); +#endif + + return env->NewStringUTF(editor_setting.utf8().get_data()); +} + JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params) { Object *obj = ObjectDB::get_instance(ObjectID(ID)); ERR_FAIL_NULL(obj); - int res = env->PushLocalFrame(16); - ERR_FAIL_COND(res != 0); - String str_method = jstring_to_string(method, env); int count = env->GetArrayLength(params); + Variant *vlist = (Variant *)alloca(sizeof(Variant) * count); - Variant **vptr = (Variant **)alloca(sizeof(Variant *) * count); + const Variant **vptr = (const Variant **)alloca(sizeof(Variant *) * count); + for (int i = 0; i < count; i++) { jobject jobj = env->GetObjectArrayElement(params, i); - Variant v; - if (jobj) { - v = _jobject_to_variant(env, jobj); - } - memnew_placement(&vlist[i], Variant); - vlist[i] = v; + ERR_FAIL_NULL(jobj); + memnew_placement(&vlist[i], Variant(_jobject_to_variant(env, jobj))); vptr[i] = &vlist[i]; env->DeleteLocalRef(jobj); } Callable::CallError err; - obj->callp(str_method, (const Variant **)vptr, count, err); - - env->PopLocalFrame(nullptr); + obj->callp(str_method, vptr, count, err); } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params) { Object *obj = ObjectDB::get_instance(ObjectID(ID)); ERR_FAIL_NULL(obj); - int res = env->PushLocalFrame(16); - ERR_FAIL_COND(res != 0); - String str_method = jstring_to_string(method, env); int count = env->GetArrayLength(params); @@ -473,16 +478,13 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv * for (int i = 0; i < count; i++) { jobject jobj = env->GetObjectArrayElement(params, i); - if (jobj) { - args[i] = _jobject_to_variant(env, jobj); - } - env->DeleteLocalRef(jobj); + ERR_FAIL_NULL(jobj); + memnew_placement(&args[i], Variant(_jobject_to_variant(env, jobj))); argptrs[i] = &args[i]; + env->DeleteLocalRef(jobj); } - MessageQueue::get_singleton()->push_callp(obj, str_method, (const Variant **)argptrs, count); - - env->PopLocalFrame(nullptr); + MessageQueue::get_singleton()->push_callp(obj, str_method, argptrs, count); } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) { diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 0020ddffd2..ee6a19034c 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -37,9 +37,9 @@ // These functions can be called from within JAVA and are the means by which our JAVA implementation calls back into our C++ code. // See java/src/org/godotengine/godot/GodotLib.java for the JAVA side of this (yes that's why we have the long names) extern "C" { -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion, jobject p_godot_tts); +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz); -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline); +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline, jobject p_godot_tts); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jobject p_surface, jint p_width, jint p_height); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jobject p_surface); JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); @@ -49,7 +49,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JN JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jboolean p_double_tap); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_factor); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed, jboolean p_echo); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jclass clazz, jint p_device, jint p_axis, jfloat p_value); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jclass clazz, jint p_device, jint p_hat_x, jint p_hat_y); @@ -61,6 +61,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gyroscope(JNIEnv *env JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz); JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jclass clazz, jstring path); +JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getEditorSetting(JNIEnv *env, jclass clazz, jstring p_setting_key); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height); diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp index b50d4870bd..a95f762e01 100644 --- a/platform/android/java_godot_view_wrapper.cpp +++ b/platform/android/java_godot_view_wrapper.cpp @@ -49,6 +49,8 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) { _request_pointer_capture = env->GetMethodID(_cls, "requestPointerCapture", "()V"); _release_pointer_capture = env->GetMethodID(_cls, "releasePointerCapture", "()V"); } + + _can_capture_pointer = env->GetMethodID(_cls, "canCapturePointer", "()Z"); } bool GodotJavaViewWrapper::can_update_pointer_icon() const { @@ -56,7 +58,16 @@ bool GodotJavaViewWrapper::can_update_pointer_icon() const { } bool GodotJavaViewWrapper::can_capture_pointer() const { - return _request_pointer_capture != nullptr && _release_pointer_capture != nullptr; + // We can capture the pointer if the other jni capture method ids are initialized, + // and GodotView#canCapturePointer() returns true. + if (_request_pointer_capture != nullptr && _release_pointer_capture != nullptr && _can_capture_pointer != nullptr) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, false); + + return env->CallBooleanMethod(_godot_view, _can_capture_pointer); + } + + return false; } void GodotJavaViewWrapper::request_pointer_capture() { diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h index 9b64ded29c..e5b04e4866 100644 --- a/platform/android/java_godot_view_wrapper.h +++ b/platform/android/java_godot_view_wrapper.h @@ -31,12 +31,13 @@ #ifndef JAVA_GODOT_VIEW_WRAPPER_H #define JAVA_GODOT_VIEW_WRAPPER_H +#include "string_android.h" + #include "core/math/vector2.h" + #include <android/log.h> #include <jni.h> -#include "string_android.h" - // Class that makes functions in java/src/org/godotengine/godot/GodotView.java callable from C++ class GodotJavaViewWrapper { private: @@ -44,6 +45,7 @@ private: jobject _godot_view; + jmethodID _can_capture_pointer = 0; jmethodID _request_pointer_capture = 0; jmethodID _release_pointer_capture = 0; diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 9d9d087896..862d9f0436 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -70,6 +70,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _request_permission = p_env->GetMethodID(godot_class, "requestPermission", "(Ljava/lang/String;)Z"); _request_permissions = p_env->GetMethodID(godot_class, "requestPermissions", "()Z"); _get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;"); + _get_ca_certificates = p_env->GetMethodID(godot_class, "getCACertificates", "()Ljava/lang/String;"); _init_input_devices = p_env->GetMethodID(godot_class, "initInputDevices", "()V"); _get_surface = p_env->GetMethodID(godot_class, "getSurface", "()Landroid/view/Surface;"); _is_activity_resumed = p_env->GetMethodID(godot_class, "isActivityResumed", "()Z"); @@ -79,6 +80,9 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I"); _get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); + _begin_benchmark_measure = p_env->GetMethodID(godot_class, "beginBenchmarkMeasure", "(Ljava/lang/String;)V"); + _end_benchmark_measure = p_env->GetMethodID(godot_class, "endBenchmarkMeasure", "(Ljava/lang/String;)V"); + _dump_benchmark = p_env->GetMethodID(godot_class, "dumpBenchmark", "(Ljava/lang/String;)V"); // get some Activity method pointers... _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); @@ -310,6 +314,17 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const { return permissions_list; } +String GodotJavaWrapper::get_ca_certificates() const { + if (_get_ca_certificates) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, String()); + jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_ca_certificates); + return jstring_to_string(s, env); + } else { + return String(); + } +} + void GodotJavaWrapper::init_input_devices() { if (_init_input_devices) { JNIEnv *env = get_jni_env(); @@ -359,3 +374,30 @@ int GodotJavaWrapper::create_new_godot_instance(List<String> args) { return 0; } } + +void GodotJavaWrapper::begin_benchmark_measure(const String &p_label) { + if (_begin_benchmark_measure) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); + env->CallVoidMethod(godot_instance, _begin_benchmark_measure, j_label); + } +} + +void GodotJavaWrapper::end_benchmark_measure(const String &p_label) { + if (_end_benchmark_measure) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); + env->CallVoidMethod(godot_instance, _end_benchmark_measure, j_label); + } +} + +void GodotJavaWrapper::dump_benchmark(const String &benchmark_file) { + if (_dump_benchmark) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_benchmark_file = env->NewStringUTF(benchmark_file.utf8().get_data()); + env->CallVoidMethod(godot_instance, _dump_benchmark, j_benchmark_file); + } +} diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 1bd79584d8..1efdffd71b 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -31,13 +31,14 @@ #ifndef JAVA_GODOT_WRAPPER_H #define JAVA_GODOT_WRAPPER_H -#include <android/log.h> -#include <jni.h> - -#include "core/templates/list.h" #include "java_godot_view_wrapper.h" #include "string_android.h" +#include "core/templates/list.h" + +#include <android/log.h> +#include <jni.h> + // Class that makes functions in java/src/org/godotengine/godot/Godot.java callable from C++ class GodotJavaWrapper { private: @@ -60,6 +61,7 @@ private: jmethodID _request_permission = nullptr; jmethodID _request_permissions = nullptr; jmethodID _get_granted_permissions = nullptr; + jmethodID _get_ca_certificates = nullptr; jmethodID _init_input_devices = nullptr; jmethodID _get_surface = nullptr; jmethodID _is_activity_resumed = nullptr; @@ -70,6 +72,9 @@ private: jmethodID _get_class_loader = nullptr; jmethodID _create_new_godot_instance = nullptr; jmethodID _get_render_view = nullptr; + jmethodID _begin_benchmark_measure = nullptr; + jmethodID _end_benchmark_measure = nullptr; + jmethodID _dump_benchmark = nullptr; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); @@ -98,12 +103,16 @@ public: bool request_permission(const String &p_name); bool request_permissions(); Vector<String> get_granted_permissions() const; + String get_ca_certificates() const; void init_input_devices(); jobject get_surface(); bool is_activity_resumed(); void vibrate(int p_duration_ms); String get_input_fallback_mapping(); int create_new_godot_instance(List<String> args); + void begin_benchmark_measure(const String &p_label); + void end_benchmark_measure(const String &p_label); + void dump_benchmark(const String &benchmark_file); }; #endif // JAVA_GODOT_WRAPPER_H diff --git a/platform/android/jni_utils.h b/platform/android/jni_utils.h index d1a4082ae5..c608f9ebaa 100644 --- a/platform/android/jni_utils.h +++ b/platform/android/jni_utils.h @@ -32,8 +32,10 @@ #define JNI_UTILS_H #include "string_android.h" -#include <core/config/engine.h> -#include <core/variant/variant.h> + +#include "core/config/engine.h" +#include "core/variant/variant.h" + #include <jni.h> struct jvalret { diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 725fea8d54..c040d8c4c6 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -30,25 +30,24 @@ #include "os_android.h" +#include "dir_access_jandroid.h" +#include "display_server_android.h" +#include "file_access_android.h" +#include "file_access_filesystem_jandroid.h" +#include "java_godot_io_wrapper.h" +#include "java_godot_wrapper.h" +#include "net_socket_android.h" + #include "core/config/project_settings.h" #include "drivers/unix/dir_access_unix.h" #include "drivers/unix/file_access_unix.h" #include "main/main.h" -#include "platform/android/display_server_android.h" #include "scene/main/scene_tree.h" #include "servers/rendering_server.h" -#include "dir_access_jandroid.h" -#include "file_access_android.h" -#include "file_access_filesystem_jandroid.h" -#include "net_socket_android.h" - #include <dlfcn.h> #include <sys/system_properties.h> -#include "java_godot_io_wrapper.h" -#include "java_godot_wrapper.h" - const char *OS_Android::ANDROID_EXEC_PATH = "apk"; String _remove_symlink(const String &dir) { @@ -168,7 +167,7 @@ Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_han } p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); - ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + "."); + 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; @@ -182,7 +181,7 @@ String OS_Android::get_name() const { } String OS_Android::get_system_property(const char *key) const { - static String value; + String value; char value_str[PROP_VALUE_MAX]; if (__system_property_get(key, value_str)) { value = String(value_str); @@ -230,20 +229,20 @@ String OS_Android::get_version() const { "ro.potato.version", "ro.xtended.version", "org.evolution.version", "ro.corvus.version", "ro.pa.version", "ro.crdroid.version", "ro.syberia.version", "ro.arrow.version", "ro.lineage.version" }; for (int i = 0; i < roms.size(); i++) { - static String rom_version = get_system_property(roms[i]); + String rom_version = get_system_property(roms[i]); if (!rom_version.is_empty()) { return rom_version; } } - static String mod_version = get_system_property("ro.modversion"); // Handles other Android custom ROMs. + String mod_version = get_system_property("ro.modversion"); // Handles other Android custom ROMs. if (!mod_version.is_empty()) { return mod_version; } // Handles stock Android. - static String sdk_version = get_system_property("ro.build.version.sdk_int"); - static String build = get_system_property("ro.build.version.incremental"); + String sdk_version = get_system_property("ro.build.version.sdk_int"); + String build = get_system_property("ro.build.version.incremental"); if (!sdk_version.is_empty()) { if (!build.is_empty()) { return vformat("%s.%s", sdk_version, build); @@ -311,7 +310,11 @@ String OS_Android::get_resource_dir() const { #ifdef TOOLS_ENABLED return OS_Unix::get_resource_dir(); #else - return "/"; //android has its own filesystem for resources inside the APK + if (remote_fs_dir.is_empty()) { + return "/"; // Android has its own filesystem for resources inside the APK + } else { + return remote_fs_dir; + } #endif } @@ -671,6 +674,27 @@ String OS_Android::get_config_path() const { return get_user_data_dir().path_join("config"); } +void OS_Android::benchmark_begin_measure(const String &p_what) { +#ifdef TOOLS_ENABLED + godot_java->begin_benchmark_measure(p_what); +#endif +} + +void OS_Android::benchmark_end_measure(const String &p_what) { +#ifdef TOOLS_ENABLED + godot_java->end_benchmark_measure(p_what); +#endif +} + +void OS_Android::benchmark_dump() { +#ifdef TOOLS_ENABLED + if (!is_use_benchmark_set()) { + return; + } + godot_java->dump_benchmark(get_benchmark_file()); +#endif +} + bool OS_Android::_check_internal_feature_support(const String &p_feature) { if (p_feature == "system_fonts") { return true; @@ -753,5 +777,19 @@ Error OS_Android::kill(const ProcessID &p_pid) { return OS_Unix::kill(p_pid); } +String OS_Android::get_system_ca_certificates() { + return godot_java->get_ca_certificates(); +} + +Error OS_Android::setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) { + r_project_path = get_user_data_dir(); + Error err = OS_Unix::setup_remote_filesystem(p_server_host, p_port, p_password, r_project_path); + if (err == OK) { + remote_fs_dir = r_project_path; + FileAccess::make_default<FileAccessFilesystemJAndroid>(FileAccess::ACCESS_RESOURCES); + } + return err; +} + OS_Android::~OS_Android() { } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index 53910b1498..abcc412588 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -32,6 +32,7 @@ #define OS_ANDROID_H #include "audio_driver_opensl.h" + #include "core/os/main_loop.h" #include "drivers/unix/os_unix.h" #include "servers/audio_server.h" @@ -57,6 +58,7 @@ private: mutable String data_dir_cache; mutable String cache_dir_cache; + mutable String remote_fs_dir; AudioDriverOpenSL audio_driver_android; @@ -159,6 +161,13 @@ public: virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override; virtual Error kill(const ProcessID &p_pid) override; + virtual String get_system_ca_certificates() override; + + virtual Error setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) override; + + virtual void benchmark_begin_measure(const String &p_what) override; + virtual void benchmark_end_measure(const String &p_what) override; + virtual void benchmark_dump() override; virtual bool _check_internal_feature_support(const String &p_feature) override; OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion); diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp index 4bb90cb971..5d48c4e248 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -30,12 +30,13 @@ #include "godot_plugin_jni.h" -#include <core/config/engine.h> -#include <core/config/project_settings.h> -#include <core/error/error_macros.h> -#include <platform/android/api/jni_singleton.h> -#include <platform/android/jni_utils.h> -#include <platform/android/string_android.h> +#include "api/jni_singleton.h" +#include "jni_utils.h" +#include "string_android.h" + +#include "core/config/engine.h" +#include "core/config/project_settings.h" +#include "core/error/error_macros.h" static HashMap<String, JNISingleton *> jni_singletons; @@ -120,7 +121,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitS for (int i = 0; i < count; i++) { jobject j_param = env->GetObjectArrayElement(j_signal_params, i); - variant_params[i] = _jobject_to_variant(env, j_param); + ERR_FAIL_NULL(j_param); + memnew_placement(&variant_params[i], Variant(_jobject_to_variant(env, j_param))); args[i] = &variant_params[i]; env->DeleteLocalRef(j_param); } diff --git a/platform/android/string_android.h b/platform/android/string_android.h index fe2f2e20a7..3f30b8ec3d 100644 --- a/platform/android/string_android.h +++ b/platform/android/string_android.h @@ -31,8 +31,10 @@ #ifndef STRING_ANDROID_H #define STRING_ANDROID_H -#include "core/string/ustring.h" #include "thread_jandroid.h" + +#include "core/string/ustring.h" + #include <jni.h> /** diff --git a/platform/android/thread_jandroid.cpp b/platform/android/thread_jandroid.cpp index 5051c179ed..9f4140f70f 100644 --- a/platform/android/thread_jandroid.cpp +++ b/platform/android/thread_jandroid.cpp @@ -30,10 +30,10 @@ #include "thread_jandroid.h" -#include <android/log.h> - #include "core/os/thread.h" +#include <android/log.h> + static JavaVM *java_vm = nullptr; static thread_local JNIEnv *env = nullptr; diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp index c08c1d4941..aef59c2584 100644 --- a/platform/android/tts_android.cpp +++ b/platform/android/tts_android.cpp @@ -35,9 +35,11 @@ #include "string_android.h" #include "thread_jandroid.h" +bool TTS_Android::initialized = false; jobject TTS_Android::tts = nullptr; jclass TTS_Android::cls = nullptr; +jmethodID TTS_Android::_init = nullptr; jmethodID TTS_Android::_is_speaking = nullptr; jmethodID TTS_Android::_is_paused = nullptr; jmethodID TTS_Android::_get_voices = nullptr; @@ -49,23 +51,34 @@ jmethodID TTS_Android::_stop_speaking = nullptr; HashMap<int, Char16String> TTS_Android::ids; void TTS_Android::setup(jobject p_tts) { - JNIEnv *env = get_jni_env(); + bool tts_enabled = GLOBAL_GET("audio/general/text_to_speech"); + if (tts_enabled) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + + tts = env->NewGlobalRef(p_tts); - tts = env->NewGlobalRef(p_tts); + jclass c = env->GetObjectClass(tts); + cls = (jclass)env->NewGlobalRef(c); - jclass c = env->GetObjectClass(tts); - cls = (jclass)env->NewGlobalRef(c); + _init = env->GetMethodID(cls, "init", "()V"); + _is_speaking = env->GetMethodID(cls, "isSpeaking", "()Z"); + _is_paused = env->GetMethodID(cls, "isPaused", "()Z"); + _get_voices = env->GetMethodID(cls, "getVoices", "()[Ljava/lang/String;"); + _speak = env->GetMethodID(cls, "speak", "(Ljava/lang/String;Ljava/lang/String;IFFIZ)V"); + _pause_speaking = env->GetMethodID(cls, "pauseSpeaking", "()V"); + _resume_speaking = env->GetMethodID(cls, "resumeSpeaking", "()V"); + _stop_speaking = env->GetMethodID(cls, "stopSpeaking", "()V"); - _is_speaking = env->GetMethodID(cls, "isSpeaking", "()Z"); - _is_paused = env->GetMethodID(cls, "isPaused", "()Z"); - _get_voices = env->GetMethodID(cls, "getVoices", "()[Ljava/lang/String;"); - _speak = env->GetMethodID(cls, "speak", "(Ljava/lang/String;Ljava/lang/String;IFFIZ)V"); - _pause_speaking = env->GetMethodID(cls, "pauseSpeaking", "()V"); - _resume_speaking = env->GetMethodID(cls, "resumeSpeaking", "()V"); - _stop_speaking = env->GetMethodID(cls, "stopSpeaking", "()V"); + if (_init) { + env->CallVoidMethod(tts, _init); + initialized = true; + } + } } 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)) { int pos = 0; if ((DisplayServer::TTSUtteranceEvent)p_event == DisplayServer::TTS_UTTERANCE_BOUNDARY) { @@ -86,6 +99,7 @@ void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) { } bool TTS_Android::is_speaking() { + ERR_FAIL_COND_V_MSG(!initialized, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_is_speaking) { JNIEnv *env = get_jni_env(); @@ -97,6 +111,7 @@ bool TTS_Android::is_speaking() { } bool TTS_Android::is_paused() { + ERR_FAIL_COND_V_MSG(!initialized, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_is_paused) { JNIEnv *env = get_jni_env(); @@ -108,6 +123,7 @@ bool TTS_Android::is_paused() { } Array TTS_Android::get_voices() { + ERR_FAIL_COND_V_MSG(!initialized, Array(), "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); Array list; if (_get_voices) { JNIEnv *env = get_jni_env(); @@ -135,6 +151,7 @@ Array TTS_Android::get_voices() { } void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (p_interrupt) { stop(); } @@ -157,6 +174,7 @@ void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volum } void TTS_Android::pause() { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_pause_speaking) { JNIEnv *env = get_jni_env(); @@ -166,6 +184,7 @@ void TTS_Android::pause() { } void TTS_Android::resume() { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_resume_speaking) { JNIEnv *env = get_jni_env(); @@ -175,6 +194,7 @@ void TTS_Android::resume() { } void TTS_Android::stop() { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); for (const KeyValue<int, Char16String> &E : ids) { DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, E.key); } diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h index 8e3bfccbdb..39efef6ed1 100644 --- a/platform/android/tts_android.h +++ b/platform/android/tts_android.h @@ -31,16 +31,20 @@ #ifndef TTS_ANDROID_H #define TTS_ANDROID_H +#include "core/config/project_settings.h" #include "core/string/ustring.h" +#include "core/templates/hash_map.h" #include "core/variant/array.h" #include "servers/display_server.h" #include <jni.h> class TTS_Android { + static bool initialized; static jobject tts; static jclass cls; + static jmethodID _init; static jmethodID _is_speaking; static jmethodID _is_paused; static jmethodID _get_voices; diff --git a/platform/android/vulkan/vulkan_context_android.cpp b/platform/android/vulkan_context_android.cpp index ce4b1b7967..01e6d14438 100644 --- a/platform/android/vulkan/vulkan_context_android.cpp +++ b/platform/android/vulkan_context_android.cpp @@ -28,10 +28,10 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifdef VULKAN_ENABLED - #include "vulkan_context_android.h" +#ifdef VULKAN_ENABLED + #ifdef USE_VOLK #include <volk.h> #else diff --git a/platform/android/vulkan/vulkan_context_android.h b/platform/android/vulkan_context_android.h index f253149ef6..f253149ef6 100644 --- a/platform/android/vulkan/vulkan_context_android.h +++ b/platform/android/vulkan_context_android.h |