diff options
Diffstat (limited to 'platform/android')
56 files changed, 4090 insertions, 438 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index d031d14499..ad226255bc 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -8,6 +8,7 @@ android_files = [ "file_access_android.cpp", "audio_driver_opensl.cpp", "dir_access_jandroid.cpp", + "tts_android.cpp", "thread_jandroid.cpp", "net_socket_android.cpp", "java_godot_lib_jni.cpp", @@ -53,10 +54,17 @@ else: if lib_arch_dir != "": if env["target"] == "release": lib_type_dir = "release" - else: # release_debug, debug + elif env["target"] == "release_debug": lib_type_dir = "debug" + else: # debug + lib_type_dir = "dev" - out_dir = "#platform/android/java/lib/libs/" + lib_type_dir + "/" + lib_arch_dir + if env["tools"]: + lib_tools_dir = "tools/" + else: + lib_tools_dir = "" + + out_dir = "#platform/android/java/lib/libs/" + lib_tools_dir + lib_type_dir + "/" + lib_arch_dir env_android.Command( out_dir + "/libgodot_android.so", "#bin/libgodot" + env["SHLIBSUFFIX"], Move("$TARGET", "$SOURCE") ) diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 8495d2cc18..dcaa586e3b 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -75,7 +75,7 @@ void AudioDriverOpenSL::_buffer_callback( void AudioDriverOpenSL::_buffer_callbacks( SLAndroidSimpleBufferQueueItf queueItf, void *pContext) { - AudioDriverOpenSL *ad = (AudioDriverOpenSL *)pContext; + AudioDriverOpenSL *ad = static_cast<AudioDriverOpenSL *>(pContext); ad->_buffer_callback(queueItf); } @@ -208,7 +208,7 @@ void AudioDriverOpenSL::_record_buffer_callback(SLAndroidSimpleBufferQueueItf qu } void AudioDriverOpenSL::_record_buffer_callbacks(SLAndroidSimpleBufferQueueItf queueItf, void *pContext) { - AudioDriverOpenSL *ad = (AudioDriverOpenSL *)pContext; + AudioDriverOpenSL *ad = static_cast<AudioDriverOpenSL *>(pContext); ad->_record_buffer_callback(queueItf); } diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 7fb4f54fca..e2b1c757d6 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -42,7 +42,7 @@ jmethodID DirAccessJAndroid::_dir_next = nullptr; jmethodID DirAccessJAndroid::_dir_close = nullptr; jmethodID DirAccessJAndroid::_dir_is_dir = nullptr; -DirAccess *DirAccessJAndroid::create_fs() { +Ref<DirAccess> DirAccessJAndroid::create_fs() { return memnew(DirAccessJAndroid); } @@ -143,7 +143,7 @@ Error DirAccessJAndroid::change_dir(String p_dir) { return OK; } -String DirAccessJAndroid::get_current_dir(bool p_include_drive) { +String DirAccessJAndroid::get_current_dir(bool p_include_drive) const { return "res://" + current_dir; } @@ -155,9 +155,9 @@ bool DirAccessJAndroid::file_exists(String p_file) { sd = current_dir.plus_file(p_file); } - FileAccessAndroid *f = memnew(FileAccessAndroid); + Ref<FileAccessAndroid> f; + f.instantiate(); bool exists = f->file_exists(sd); - memdelete(f); return exists; } diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index 4f4a984b12..3d3c9f9223 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -51,7 +51,7 @@ class DirAccessJAndroid : public DirAccess { String current_dir; String current; - static DirAccess *create_fs(); + static Ref<DirAccess> create_fs(); public: virtual Error list_dir_begin(); ///< This starts dir listing @@ -64,7 +64,7 @@ public: virtual String get_drive(int p_drive); virtual Error change_dir(String p_dir); ///< can be relative or absolute, return false on success - virtual String get_current_dir(bool p_include_drive = true); ///< return current dir location + virtual String get_current_dir(bool p_include_drive = true) const; ///< return current dir location virtual bool file_exists(String p_file); virtual bool dir_exists(String p_dir); diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index a7a8801bdc..2eb7056a36 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -34,6 +34,7 @@ #include "java_godot_io_wrapper.h" #include "java_godot_wrapper.h" #include "os_android.h" +#include "tts_android.h" #if defined(VULKAN_ENABLED) #include "drivers/vulkan/rendering_device_vulkan.h" @@ -42,7 +43,7 @@ #endif DisplayServerAndroid *DisplayServerAndroid::get_singleton() { - return (DisplayServerAndroid *)DisplayServer::get_singleton(); + return static_cast<DisplayServerAndroid *>(DisplayServer::get_singleton()); } bool DisplayServerAndroid::has_feature(Feature p_feature) const { @@ -63,6 +64,7 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const { case FEATURE_ORIENTATION: case FEATURE_TOUCHSCREEN: case FEATURE_VIRTUAL_KEYBOARD: + case FEATURE_TEXT_TO_SPEECH: return true; default: return false; @@ -73,6 +75,34 @@ String DisplayServerAndroid::get_name() const { return "Android"; } +bool DisplayServerAndroid::tts_is_speaking() const { + return TTS_Android::is_speaking(); +} + +bool DisplayServerAndroid::tts_is_paused() const { + return TTS_Android::is_paused(); +} + +Array DisplayServerAndroid::tts_get_voices() const { + return TTS_Android::get_voices(); +} + +void DisplayServerAndroid::tts_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) { + TTS_Android::speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt); +} + +void DisplayServerAndroid::tts_pause() { + TTS_Android::pause(); +} + +void DisplayServerAndroid::tts_resume() { + TTS_Android::resume(); +} + +void DisplayServerAndroid::tts_stop() { + TTS_Android::stop(); +} + void DisplayServerAndroid::clipboard_set(const String &p_text) { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); ERR_FAIL_COND(!godot_java); @@ -161,6 +191,13 @@ int DisplayServerAndroid::screen_get_dpi(int p_screen) const { return godot_io_java->get_screen_dpi(); } +float DisplayServerAndroid::screen_get_scale(int p_screen) const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_COND_V(!godot_io_java, 1.0f); + + return godot_io_java->get_scaled_density(); +} + float DisplayServerAndroid::screen_get_refresh_rate(int p_screen) const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); if (!godot_io_java) { @@ -270,7 +307,7 @@ int64_t DisplayServerAndroid::window_get_native_handle(HandleType p_handle_type, return 0; // Not supported. } case WINDOW_HANDLE: { - return (int64_t)((OS_Android *)OS::get_singleton())->get_godot_java()->get_activity(); + return reinterpret_cast<int64_t>(static_cast<OS_Android *>(OS::get_singleton())->get_godot_java()->get_activity()); } case WINDOW_VIEW: { return 0; // Not supported. diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index 23077a6529..2604214ac0 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -91,6 +91,15 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + virtual void clipboard_set(const String &p_text) override; virtual String clipboard_get() const override; virtual bool clipboard_has() const override; @@ -106,6 +115,7 @@ public: virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index df3693ba61..8cfa3a67b9 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -249,7 +249,7 @@ static const int DEFAULT_TARGET_SDK_VERSION = 30; // Should match the value in ' const String SDK_VERSION_RANGE = vformat("%s,%s,1", DEFAULT_MIN_SDK_VERSION, DEFAULT_TARGET_SDK_VERSION); void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { - EditorExportPlatformAndroid *ea = (EditorExportPlatformAndroid *)ud; + EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud); while (!ea->quit_request.is_set()) { // Check for plugins updates @@ -596,8 +596,8 @@ Vector<String> EditorExportPlatformAndroid::get_abis() { /// List the gdap files in the directory specified by the p_path parameter. Vector<String> EditorExportPlatformAndroid::list_gdap_files(const String &p_path) { Vector<String> dir_files; - DirAccessRef da = DirAccess::open(p_path); - if (da) { + Ref<DirAccess> da = DirAccess::open(p_path); + if (da.is_valid()) { da->list_dir_begin(); while (true) { String file = da->get_next(); @@ -685,7 +685,7 @@ Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObj ERR_PRINT(err); return FAILED; } - APKExportData *ed = (APKExportData *)p_userdata; + APKExportData *ed = static_cast<APKExportData *>(p_userdata); Vector<String> abis = get_abis(); bool exported = false; for (int i = 0; i < p_so.tags.size(); ++i) { @@ -710,7 +710,7 @@ Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObj } Error EditorExportPlatformAndroid::save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { - APKExportData *ed = (APKExportData *)p_userdata; + APKExportData *ed = static_cast<APKExportData *>(p_userdata); String dst_path = p_path.replace_first("res://", "assets/"); store_in_apk(ed, dst_path, p_data, _should_compress_asset(p_path, p_data) ? Z_DEFLATED : 0); @@ -725,7 +725,7 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared ERR_FAIL_COND_V_MSG(!p_so.path.get_file().begins_with("lib"), FAILED, "Android .so file names must start with \"lib\", but got: " + p_so.path); Vector<String> abis = get_abis(); - CustomExportData *export_data = (CustomExportData *)p_userdata; + CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata); bool exported = false; for (int i = 0; i < p_so.tags.size(); ++i) { int abi_index = abis.find(p_so.tags[i]); @@ -863,6 +863,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p bool classify_as_game = p_preset->get("package/classify_as_game"); bool retain_data_on_uninstall = p_preset->get("package/retain_data_on_uninstall"); bool exclude_from_recents = p_preset->get("package/exclude_from_recents"); + bool is_resizeable = bool(GLOBAL_GET("display/window/size/resizable")); Vector<String> perms; // Write permissions into the perms variable. @@ -980,6 +981,10 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p encode_uint32(exclude_from_recents, &p_manifest.write[iofs + 16]); } + if (tname == "activity" && attrname == "resizeableActivity") { + encode_uint32(is_resizeable, &p_manifest.write[iofs + 16]); + } + if (tname == "supports-screens") { if (attrname == "smallScreens") { encode_uint32(screen_support_small ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]); @@ -2003,7 +2008,7 @@ String EditorExportPlatformAndroid::get_apksigner_path() { Error errn; String build_tools_dir = sdk_path.plus_file("build-tools"); - DirAccessRef da = DirAccess::open(build_tools_dir, &errn); + Ref<DirAccess> da = DirAccess::open(build_tools_dir, &errn); if (errn != OK) { print_error("Unable to open Android 'build-tools' directory."); return apksigner_path; @@ -2120,7 +2125,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr } else { Error errn; // Check for the platform-tools directory. - DirAccessRef da = DirAccess::open(sdk_path.plus_file("platform-tools"), &errn); + Ref<DirAccess> da = DirAccess::open(sdk_path.plus_file("platform-tools"), &errn); if (errn != OK) { err += TTR("Invalid Android SDK path in Editor Settings."); err += TTR("Missing 'platform-tools' directory!"); @@ -2138,7 +2143,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr } // Check for the build-tools directory. - DirAccessRef build_tools_da = DirAccess::open(sdk_path.plus_file("build-tools"), &errn); + Ref<DirAccess> build_tools_da = DirAccess::open(sdk_path.plus_file("build-tools"), &errn); if (errn != OK) { err += TTR("Invalid Android SDK path in Editor Settings."); err += TTR("Missing 'build-tools' directory!"); @@ -2417,12 +2422,12 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } void EditorExportPlatformAndroid::_clear_assets_directory() { - DirAccessRef da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); + Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); // Clear the APK assets directory if (da_res->dir_exists(APK_ASSETS_DIRECTORY)) { print_verbose("Clearing APK assets directory.."); - DirAccessRef da_assets = DirAccess::open(APK_ASSETS_DIRECTORY); + Ref<DirAccess> da_assets = DirAccess::open(APK_ASSETS_DIRECTORY); da_assets->erase_contents_recursive(); da_res->remove(APK_ASSETS_DIRECTORY); } @@ -2430,7 +2435,7 @@ void EditorExportPlatformAndroid::_clear_assets_directory() { // Clear the AAB assets directory if (da_res->dir_exists(AAB_ASSETS_DIRECTORY)) { print_verbose("Clearing AAB assets directory.."); - DirAccessRef da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY); + Ref<DirAccess> da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY); da_assets->erase_contents_recursive(); da_res->remove(AAB_ASSETS_DIRECTORY); } @@ -2450,7 +2455,7 @@ void EditorExportPlatformAndroid::_remove_copied_libs() { ERR_FAIL_COND_MSG(error, "Error parsing \"" + libs_json + "\" on line " + itos(json.get_error_line()) + ": " + json.get_error_message()); Vector<String> libs = json.get_data(); - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES); for (int i = 0; i < libs.size(); i++) { print_verbose("Removing previously installed library " + libs[i]); da->remove(libs[i]); @@ -2539,14 +2544,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP //test that installed build version is alright { print_verbose("Checking build version.."); - FileAccessRef f = FileAccess::open("res://android/.build_version", FileAccess::READ); - if (!f) { + Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ); + if (f.is_null()) { EditorNode::get_singleton()->show_warning(TTR("Trying to build from a custom built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); return ERR_UNCONFIGURED; } String version = f->get_line().strip_edges(); print_verbose("- build version: " + version); - f->close(); if (version != VERSION_FULL_CONFIG) { EditorNode::get_singleton()->show_warning(vformat(TTR("Android build version mismatch:\n Template installed: %s\n Godot Version: %s\nPlease reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG)); return ERR_UNCONFIGURED; @@ -2582,10 +2586,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return err; } if (user_data.libs.size() > 0) { - FileAccessRef fa = FileAccess::open(GDNATIVE_LIBS_PATH, FileAccess::WRITE); + Ref<FileAccess> fa = FileAccess::open(GDNATIVE_LIBS_PATH, FileAccess::WRITE); JSON json; fa->store_string(json.stringify(user_data.libs, "\t")); - fa->close(); } } else { print_verbose("Saving apk expansion file.."); @@ -2765,8 +2768,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return ERR_FILE_BAD_PATH; } - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + zlib_filefunc_def io = zipio_create_io(); if (ep.step(TTR("Creating APK..."), 0)) { return ERR_SKIP; @@ -2780,9 +2782,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP int ret = unzGoToFirstFile(pkg); - zlib_filefunc_def io2 = io; - FileAccess *dst_f = nullptr; - io2.opaque = &dst_f; + zlib_filefunc_def io2 = zipio_create_io(); String tmp_unaligned_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk"); @@ -2807,6 +2807,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP unz_file_info info; char fname[16384]; ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); + if (ret != UNZ_OK) { + break; + } bool skip = false; @@ -2975,9 +2978,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP ret = unzGoToFirstFile(tmp_unaligned); - io2 = io; - dst_f = nullptr; - io2.opaque = &dst_f; + io2 = zipio_create_io(); zipFile final_apk = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2); // Take files from the unaligned APK and write them out to the aligned one @@ -2991,6 +2992,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP char fname[16384]; char extra[16384]; ret = unzGetCurrentFileInfo(tmp_unaligned, &info, fname, 16384, extra, 16384 - ZIP_ALIGNMENT, nullptr, 0); + if (ret != UNZ_OK) { + break; + } String file = String::utf8(fname); diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index ab915a5f85..173bb8bcb7 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -75,8 +75,8 @@ String _get_android_orientation_label(DisplayServer::ScreenOrientation screen_or // Utility method used to create a directory. Error create_directory(const String &p_dir) { if (!DirAccess::exists(p_dir)) { - DirAccessRef filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES); - ERR_FAIL_COND_V_MSG(!filesystem_da, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); + Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); Error err = filesystem_da->make_dir_recursive(p_dir); ERR_FAIL_COND_V_MSG(err, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); } @@ -91,10 +91,9 @@ Error store_file_at_path(const String &p_path, const Vector<uint8_t> &p_data) { if (err != OK) { return err; } - FileAccess *fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); fa->store_buffer(p_data.ptr(), p_data.size()); - memdelete(fa); return OK; } @@ -109,10 +108,9 @@ Error store_string_at_path(const String &p_path, const String &p_data) { } return err; } - FileAccess *fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); fa->store_string(p_data); - memdelete(fa); return OK; } @@ -122,7 +120,7 @@ Error store_string_at_path(const String &p_path, const String &p_data) { // It's functionality mirrors that of the method save_apk_file. // This method will be called ONLY when custom build is enabled. Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { - CustomExportData *export_data = (CustomExportData *)p_userdata; + CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata); String dst_path = p_path.replace_first("res://", export_data->assets_directory + "/"); print_verbose("Saving project files from " + p_path + " into " + dst_path); Error err = store_file_at_path(dst_path, p_data); @@ -152,8 +150,8 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset store_string_at_path("res://android/build/res/values/godot_project_name_string.xml", processed_default_xml_string); // Searches the Gradle project res/ directory to find all supported locales - DirAccessRef da = DirAccess::open("res://android/build/res"); - if (!da) { + Ref<DirAccess> da = DirAccess::open("res://android/build/res"); + if (da.is_null()) { if (OS::get_singleton()->is_stdout_verbose()) { print_error("Unable to open Android resources directory."); } @@ -253,11 +251,13 @@ String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) { String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(GLOBAL_GET("display/window/handheld/orientation")))); String manifest_activity_text = vformat( " <activity android:name=\"com.godot.game.GodotApp\" " - "tools:replace=\"android:screenOrientation,android:excludeFromRecents\" " + "tools:replace=\"android:screenOrientation,android:excludeFromRecents,android:resizeableActivity\" " "android:excludeFromRecents=\"%s\" " - "android:screenOrientation=\"%s\">\n", + "android:screenOrientation=\"%s\" " + "android:resizeableActivity=\"%s\">\n", bool_to_string(p_preset->get("package/exclude_from_recents")), - orientation); + orientation, + bool_to_string(bool(GLOBAL_GET("display/window/size/resizable")))); if (uses_xr) { manifest_activity_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.vr.focusaware\" android:value=\"true\" />\n"; } else { diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp index c84a919b6b..d7ba31e3c9 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -34,11 +34,13 @@ AAssetManager *FileAccessAndroid::asset_manager = nullptr; -FileAccess *FileAccessAndroid::create_android() { +Ref<FileAccess> FileAccessAndroid::create_android() { return memnew(FileAccessAndroid); } Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) { + _close(); + String path = fix_path(p_path).simplify_path(); if (path.begins_with("/")) { path = path.substr(1, path.length()); @@ -58,7 +60,7 @@ Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) { return OK; } -void FileAccessAndroid::close() { +void FileAccessAndroid::_close() { if (!a) { return; } @@ -162,5 +164,5 @@ bool FileAccessAndroid::file_exists(const String &p_path) { } FileAccessAndroid::~FileAccessAndroid() { - close(); + _close(); } diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index fb612cd008..33b692da20 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -38,17 +38,18 @@ //#include <android_native_app_glue.h> class FileAccessAndroid : public FileAccess { - static FileAccess *create_android(); + static Ref<FileAccess> create_android(); mutable AAsset *a = nullptr; mutable uint64_t len = 0; mutable uint64_t pos = 0; mutable bool eof = false; + void _close(); + public: static AAssetManager *asset_manager; virtual Error _open(const String &p_path, int p_mode_flags); ///< open a file - virtual void close(); ///< close a file virtual bool is_open() const; ///< true when file is open virtual void seek(uint64_t p_position); ///< seek to a given position diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 5d1a9d7b99..b6303d1bc9 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -33,6 +33,11 @@ allprojects { } } +configurations { + // Initializes a placeholder for the devImplementation dependency configuration. + devImplementation {} +} + dependencies { implementation libraries.kotlinStdLib implementation libraries.androidxFragment @@ -45,6 +50,7 @@ dependencies { // Custom build mode. In this scenario this project is the only one around and the Godot // library is available through the pre-generated godot-lib.*.aar android archive files. debugImplementation fileTree(dir: 'libs/debug', include: ['*.jar', '*.aar']) + devImplementation fileTree(dir: 'libs/dev', include: ['*.jar', '*.aar']) releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar']) } @@ -66,6 +72,7 @@ dependencies { android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion compileOptions { sourceCompatibility versions.javaVersion @@ -93,6 +100,8 @@ android { versionName getExportVersionName() minSdkVersion getExportMinSdkVersion() targetSdkVersion getExportTargetSdkVersion() + + missingDimensionStrategy 'products', 'template' } lintOptions { @@ -146,6 +155,18 @@ android { } } + dev { + initWith debug + // Signing and zip-aligning are skipped for prebuilt builds, but + // performed for custom builds. + zipAlignEnabled shouldZipAlign() + if (shouldSign()) { + signingConfig signingConfigs.debug + } else { + signingConfig null + } + } + release { // Signing and zip-aligning are skipped for prebuilt builds, but // performed for custom builds. @@ -167,6 +188,7 @@ android { assets.srcDirs = ['assets'] } debug.jniLibs.srcDirs = ['libs/debug', 'libs/debug/vulkan_validation_layers'] + dev.jniLibs.srcDirs = ['libs/dev'] release.jniLibs.srcDirs = ['libs/release'] } @@ -183,6 +205,12 @@ task copyAndRenameDebugApk(type: Copy) { rename "android_debug.apk", getExportFilename() } +task copyAndRenameDevApk(type: Copy) { + from "$buildDir/outputs/apk/dev/android_dev.apk" + into getExportPath() + rename "android_dev.apk", getExportFilename() +} + task copyAndRenameReleaseApk(type: Copy) { from "$buildDir/outputs/apk/release/android_release.apk" into getExportPath() @@ -195,6 +223,12 @@ task copyAndRenameDebugAab(type: Copy) { rename "build-debug.aab", getExportFilename() } +task copyAndRenameDevAab(type: Copy) { + from "$buildDir/outputs/bundle/dev/build-dev.aab" + into getExportPath() + rename "build-dev.aab", getExportFilename() +} + task copyAndRenameReleaseAab(type: Copy) { from "$buildDir/outputs/bundle/release/build-release.aab" into getExportPath() diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index c238d1b361..1b2976e715 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -76,7 +76,7 @@ ext.getGodotEditorVersion = { -> String editorVersion = project.hasProperty("godot_editor_version") ? project.property("godot_editor_version") : "" if (editorVersion == null || editorVersion.isEmpty()) { // Try the library version first - editorVersion = getGodotLibraryVersion() + editorVersion = getGodotLibraryVersionName() if (editorVersion.isEmpty()) { // Fallback value. @@ -86,9 +86,24 @@ ext.getGodotEditorVersion = { -> return editorVersion } +ext.getGodotLibraryVersionCode = { -> + String versionName = "" + int versionCode = 1 + (versionName, versionCode) = getGodotLibraryVersion() + return versionCode +} + +ext.getGodotLibraryVersionName = { -> + String versionName = "" + int versionCode = 1 + (versionName, versionCode) = getGodotLibraryVersion() + return versionName +} + ext.generateGodotLibraryVersion = { List<String> requiredKeys -> // Attempt to read the version from the `version.py` file. - String libraryVersion = "" + String libraryVersionName = "" + int libraryVersionCode = 0 File versionFile = new File("../../../version.py") if (versionFile.isFile()) { @@ -109,15 +124,35 @@ ext.generateGodotLibraryVersion = { List<String> requiredKeys -> } if (requiredKeys.empty) { - libraryVersion = map.values().join(".") + libraryVersionName = map.values().join(".") + try { + if (map.containsKey("patch")) { + libraryVersionCode = Integer.parseInt(map["patch"]) + } + + if (map.containsKey("minor")) { + libraryVersionCode += (Integer.parseInt(map["minor"]) * 100) + } + + if (map.containsKey("major")) { + libraryVersionCode += (Integer.parseInt(map["major"]) * 10000) + } + } catch (NumberFormatException ignore) { + libraryVersionCode = 1 + } } } - if (libraryVersion.isEmpty()) { + if (libraryVersionName.isEmpty()) { // Fallback value in case we're unable to read the file. - libraryVersion = "custom_build" + libraryVersionName = "custom_build" + } + + if (libraryVersionCode == 0) { + libraryVersionCode = 1 } - return libraryVersion + + return [libraryVersionName, libraryVersionCode] } ext.getGodotLibraryVersion = { -> @@ -127,7 +162,10 @@ ext.getGodotLibraryVersion = { -> ext.getGodotPublishVersion = { -> List<String> requiredKeys = ["major", "minor", "patch", "status"] - return generateGodotLibraryVersion(requiredKeys) + String versionName = "" + int versionCode = 1 + (versionName, versionCode) = generateGodotLibraryVersion(requiredKeys) + return versionName } final String VALUE_SEPARATOR_REGEX = "\\|" diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index 83bc68c992..e16ca65df5 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -26,21 +26,22 @@ allprojects { ext { supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"] - supportedTargets = ["release", "debug"] + supportedTargetsMap = [release: "release", dev: "debug", debug: "release_debug"] + supportedFlavors = ["editor", "template"] - // Used by gradle to specify which architecture to build for by default when running `./gradlew build`. - // This command is usually used by Android Studio. + // Used by gradle to specify which architecture to build for by default when running + // `./gradlew build` (this command is usually used by Android Studio). // If building manually on the command line, it's recommended to use the - // `./gradlew generateGodotTemplates` build command instead after running the `scons` command. - // The defaultAbi must be one of the {supportedAbis} values. - defaultAbi = "arm64v8" + // `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s). + // The {selectedAbis} values must be from the {supportedAbis} values. + selectedAbis = ["arm64v8"] } def rootDir = "../../.." def binDir = "$rootDir/bin/" -def getSconsTaskName(String buildType) { - return "compileGodotNativeLibs" + buildType.capitalize() +def getSconsTaskName(String flavor, String buildType, String abi) { + return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize() } /** @@ -55,6 +56,17 @@ task copyDebugBinaryToBin(type: Copy) { } /** + * Copy the generated 'android_dev.apk' binary template into the Godot bin directory. + * Depends on the app build task to ensure the binary is generated prior to copying. + */ +task copyDevBinaryToBin(type: Copy) { + dependsOn ':app:assembleDev' + from('app/build/outputs/apk/dev') + into(binDir) + include('android_dev.apk') +} + +/** * Copy the generated 'android_release.apk' binary template into the Godot bin directory. * Depends on the app build task to ensure the binary is generated prior to copying. */ @@ -70,7 +82,7 @@ task copyReleaseBinaryToBin(type: Copy) { * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyDebugAARToAppModule(type: Copy) { - dependsOn ':lib:assembleDebug' + dependsOn ':lib:assembleTemplateDebug' from('lib/build/outputs/aar') into('app/libs/debug') include('godot-lib.debug.aar') @@ -81,18 +93,40 @@ task copyDebugAARToAppModule(type: Copy) { * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyDebugAARToBin(type: Copy) { - dependsOn ':lib:assembleDebug' + dependsOn ':lib:assembleTemplateDebug' from('lib/build/outputs/aar') into(binDir) include('godot-lib.debug.aar') } /** + * Copy the Godot android library archive dev file into the app module dev libs directory. + * Depends on the library build task to ensure the AAR file is generated prior to copying. + */ +task copyDevAARToAppModule(type: Copy) { + dependsOn ':lib:assembleTemplateDev' + from('lib/build/outputs/aar') + into('app/libs/dev') + include('godot-lib.dev.aar') +} + +/** + * Copy the Godot android library archive dev file into the root bin directory. + * Depends on the library build task to ensure the AAR file is generated prior to copying. + */ +task copyDevAARToBin(type: Copy) { + dependsOn ':lib:assembleTemplateDev' + from('lib/build/outputs/aar') + into(binDir) + include('godot-lib.dev.aar') +} + +/** * Copy the Godot android library archive release file into the app module release libs directory. * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyReleaseAARToAppModule(type: Copy) { - dependsOn ':lib:assembleRelease' + dependsOn ':lib:assembleTemplateRelease' from('lib/build/outputs/aar') into('app/libs/release') include('godot-lib.release.aar') @@ -103,7 +137,7 @@ task copyReleaseAARToAppModule(type: Copy) { * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyReleaseAARToBin(type: Copy) { - dependsOn ':lib:assembleRelease' + dependsOn ':lib:assembleTemplateRelease' from('lib/build/outputs/aar') into(binDir) include('godot-lib.release.aar') @@ -111,7 +145,7 @@ task copyReleaseAARToBin(type: Copy) { /** * Generate Godot custom build template by zipping the source files from the app directory, as well - * as the AAR files generated by 'copyDebugAAR' and 'copyReleaseAAR'. + * as the AAR files generated by 'copyDebugAAR', 'copyDevAAR' and 'copyReleaseAAR'. * The zip file also includes some gradle tools to allow building of the custom build. */ task zipCustomBuild(type: Zip) { @@ -130,8 +164,18 @@ def templateExcludedBuildTask() { def excludedTasks = [] if (!isAndroidStudio()) { logger.lifecycle("Excluding Android studio build tasks") - for (String buildType : supportedTargets) { - excludedTasks += ":lib:" + getSconsTaskName(buildType) + for (String flavor : supportedFlavors) { + for (String buildType : supportedTargetsMap.keySet()) { + if (buildType == "release" && flavor == "editor") { + // 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. + continue + } + + for (String abi : selectedAbis) { + excludedTasks += ":lib:" + getSconsTaskName(flavor, buildType, abi) + } + } } } return excludedTasks @@ -141,7 +185,7 @@ def templateBuildTasks() { def tasks = [] // Only build the apks and aar files for which we have native shared libraries. - for (String target : supportedTargets) { + for (String target : supportedTargetsMap.keySet()) { File targetLibs = new File("lib/libs/" + target) if (targetLibs != null && targetLibs.isDirectory() @@ -167,6 +211,50 @@ def isAndroidStudio() { return sysProps != null && sysProps['idea.platform.prefix'] != null } +task copyEditorDebugBinaryToBin(type: Copy) { + dependsOn ':editor:assembleDebug' + from('editor/build/outputs/apk/debug') + into(binDir) + include('android_editor.apk') +} + +task copyEditorDevBinaryToBin(type: Copy) { + dependsOn ':editor:assembleDev' + from('editor/build/outputs/apk/dev') + into(binDir) + include('android_editor_dev.apk') +} + +/** + * Generate the Godot Editor Android apk. + * + * Note: The Godot 'tools' shared libraries must have been generated (via scons) prior to running + * this gradle task. The task will only build the apk(s) for which the shared libraries is + * available. + */ +task generateGodotEditor { + gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() + + def tasks = [] + + for (String target : supportedTargetsMap.keySet()) { + if (target == "release") { + // 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. + continue + } + File targetLibs = new File("lib/libs/tools/" + target) + if (targetLibs != null + && targetLibs.isDirectory() + && targetLibs.listFiles() != null + && targetLibs.listFiles().length > 0) { + tasks += "copyEditor${target.capitalize()}BinaryToBin" + } + } + + dependsOn = tasks +} + /** * Master task used to coordinate the tasks defined above to generate the set of Godot templates. */ @@ -191,7 +279,27 @@ task generateDevTemplate { } /** - * Clean the generated artifacts. + * Clean the generated editor artifacts. + */ +task cleanGodotEditor(type: Delete) { + // Delete the generated native tools libs + delete("lib/libs/tools") + + // Delete the library generated AAR files + delete("lib/build/outputs/aar") + + // 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") + + finalizedBy getTasksByName("clean", true) +} + +/** + * Clean the generated template artifacts. */ task cleanGodotTemplates(type: Delete) { // Delete the generated native libs @@ -208,9 +316,11 @@ task cleanGodotTemplates(type: Delete) { // Delete the Godot templates in the Godot bin directory delete("$binDir/android_debug.apk") + delete("$binDir/android_dev.apk") delete("$binDir/android_release.apk") delete("$binDir/android_source.zip") delete("$binDir/godot-lib.debug.aar") + delete("$binDir/godot-lib.dev.aar") delete("$binDir/godot-lib.release.aar") finalizedBy getTasksByName("clean", true) diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle new file mode 100644 index 0000000000..3312f61ad3 --- /dev/null +++ b/platform/android/java/editor/build.gradle @@ -0,0 +1,74 @@ +// Gradle build config for Godot Engine's Android port. +apply plugin: 'com.android.application' + +dependencies { + implementation libraries.kotlinStdLib + implementation libraries.androidxFragment + implementation project(":lib") +} + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion + + defaultConfig { + // The 'applicationId' suffix allows to install Godot 3.x(v3) and 4.x(v4) on the same device + applicationId "org.godotengine.editor.v4" + versionCode getGodotLibraryVersionCode() + versionName getGodotLibraryVersionName() + minSdkVersion versions.minSdk + //noinspection ExpiredTargetSdkVersion - Restrict to version 29 until https://github.com/godotengine/godot/pull/51815 is submitted + targetSdkVersion 29 // versions.targetSdk + + missingDimensionStrategy 'products', 'editor' + } + + compileOptions { + sourceCompatibility versions.javaVersion + targetCompatibility versions.javaVersion + } + + buildTypes { + dev { + initWith debug + applicationIdSuffix ".dev" + } + + debug { + initWith release + + // Need to swap with the release signing config when this is ready for public release. + 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. + } + } + + packagingOptions { + // 'doNotStrip' is enabled for development within Android Studio + if (shouldNotStrip()) { + 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/dev/res/values/strings.xml b/platform/android/java/editor/src/dev/res/values/strings.xml new file mode 100644 index 0000000000..45fae3fd39 --- /dev/null +++ b/platform/android/java/editor/src/dev/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.x (dev)</string> +</resources> diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bae075d929 --- /dev/null +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="org.godotengine.editor" + android:installLocation="auto"> + + <supports-screens + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="true" + android:xlargeScreens="true" /> + + <uses-feature + android:glEsVersion="0x00020000" + android:required="true" /> + + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.INTERNET" /> + + <application + android:allowBackup="false" + android:icon="@mipmap/icon" + android:label="@string/godot_editor_name_string" + tools:ignore="GoogleAppIndexingWarning" + android:requestLegacyExternalStorage="true"> + + <activity + android:name=".GodotProjectManager" + android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + android:launchMode="singleTask" + android:screenOrientation="userLandscape" + android:exported="true" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" + android:process=":GodotProjectManager"> + + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + <activity + android:name=".GodotEditor" + android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + android:process=":GodotEditor" + android:launchMode="singleTask" + android:screenOrientation="userLandscape" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"> + </activity> + + <activity + android:name=".GodotGame" + 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:screenOrientation="userLandscape" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"> + </activity> + + </application> + +</manifest> diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java new file mode 100644 index 0000000000..8a6bf88267 --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java @@ -0,0 +1,124 @@ +/*************************************************************************/ +/* GodotEditor.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.editor; + +import org.godotengine.godot.FullScreenGodotApp; +import org.godotengine.godot.utils.PermissionsUtil; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Debug; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Base class for the Godot Android Editor activities. + * + * This provides the basic templates for the activities making up this application. + * Each derived activity runs in its own process, which enable up to have several instances of + * the Godot engine up and running at the same time. + * + * It also plays the role of the primary editor window. + */ +public class GodotEditor extends FullScreenGodotApp { + private static final boolean WAIT_FOR_DEBUGGER = false; + private static final String COMMAND_LINE_PARAMS = "command_line_params"; + + private static final String EDITOR_ARG = "--editor"; + private static final String PROJECT_MANAGER_ARG = "--project-manager"; + + private final List<String> commandLineParams = new ArrayList<>(); + + @Override + public void onCreate(Bundle savedInstanceState) { + PermissionsUtil.requestManifestPermissions(this); + + String[] params = getIntent().getStringArrayExtra(COMMAND_LINE_PARAMS); + updateCommandLineParams(params); + + if (BuildConfig.BUILD_TYPE.equals("debug") && WAIT_FOR_DEBUGGER) { + Debug.waitForDebugger(); + } + super.onCreate(savedInstanceState); + } + + private void updateCommandLineParams(@Nullable String[] args) { + // Update the list of command line params with the new args + commandLineParams.clear(); + if (args != null && args.length > 0) { + commandLineParams.addAll(Arrays.asList(args)); + } + } + + @Override + public List<String> getCommandLine() { + return commandLineParams; + } + + @Override + public void onNewGodotInstanceRequested(String[] args) { + // Parse the arguments to figure out which activity to start. + Class<?> targetClass = GodotGame.class; + for (String arg : args) { + if (EDITOR_ARG.equals(arg)) { + targetClass = GodotEditor.class; + break; + } + + if (PROJECT_MANAGER_ARG.equals(arg)) { + targetClass = GodotProjectManager.class; + break; + } + } + + // Launch a new activity + Intent newInstance = new Intent(this, targetClass).putExtra(COMMAND_LINE_PARAMS, args); + startActivity(newInstance); + } + + @Override + public void setRequestedOrientation(int requestedOrientation) { + if (!overrideOrientationRequest()) { + super.setRequestedOrientation(requestedOrientation); + } + } + + /** + * The Godot Android Editor sets its own orientation via its AndroidManifest + */ + protected boolean overrideOrientationRequest() { + return true; + } +} diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.java new file mode 100644 index 0000000000..12766775a8 --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.java @@ -0,0 +1,40 @@ +/*************************************************************************/ +/* GodotGame.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.editor; + +/** + * Drives the 'run project' window of the Godot Editor. + */ +public class GodotGame extends GodotEditor { + protected boolean overrideOrientationRequest() { + return false; + } +} diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.java new file mode 100644 index 0000000000..d30f66bb8c --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.java @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* GodotProjectManager.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.editor; + +/** + * Launcher activity for the Godot Android Editor. + * + * It presents the user with the project manager interface. + * Upon selection of a project, this activity (via its parent logic) starts the + * {@link GodotEditor} activity. + */ +public class GodotProjectManager extends GodotEditor { +} diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml new file mode 100644 index 0000000000..e8ce34f34d --- /dev/null +++ b/platform/android/java/editor/src/main/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.x</string> +</resources> diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 120a40a31d..c806de1ded 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -18,14 +18,13 @@ def pathToRootDir = "../../../../" android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools - ndkVersion versions.ndkVersion defaultConfig { minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk - manifestPlaceholders = [godotLibraryVersion: getGodotLibraryVersion()] + manifestPlaceholders = [godotLibraryVersion: getGodotLibraryVersionName()] } namespace = "org.godotengine.godot" @@ -35,6 +34,18 @@ android { targetCompatibility versions.javaVersion } + buildTypes { + dev { + initWith debug + } + } + + flavorDimensions "products" + productFlavors { + editor {} + template {} + } + lintOptions { abortOnError false disable 'MissingTranslation', 'UnusedResources' @@ -58,24 +69,50 @@ android { aidl.srcDirs = ['aidl'] assets.srcDirs = ['assets'] } + debug.jniLibs.srcDirs = ['libs/debug'] + dev.jniLibs.srcDirs = ['libs/dev'] release.jniLibs.srcDirs = ['libs/release'] + + // Editor jni library + 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 -> - variant.outputs.all { output -> - output.outputFileName = "godot-lib.${variant.name}.aar" + def flavorName = variant.getFlavorName() + if (flavorName == null || flavorName == "") { + throw new GradleException("Invalid product flavor: $flavorName") } - def buildType = variant.buildType.name.capitalize() + boolean toolsFlag = flavorName == "editor" - def releaseTarget = buildType.toLowerCase() - if (releaseTarget == null || releaseTarget == "") { - throw new GradleException("Invalid build type: " + buildType) + def buildType = variant.buildType.name + if (buildType == null || buildType == "" || !supportedTargetsMap.containsKey(buildType)) { + throw new GradleException("Invalid build type: $buildType") } - if (!supportedAbis.contains(defaultAbi)) { - throw new GradleException("Invalid default abi: " + defaultAbi) + def sconsTarget = supportedTargetsMap[buildType] + if (sconsTarget == null || sconsTarget == "") { + throw new GradleException("Invalid scons target: $sconsTarget") + } + + // Update the name of the generated library + def outputSuffix = "${buildType}.aar" + if (toolsFlag) { + outputSuffix = "tools.$outputSuffix" + } + variant.outputs.all { output -> + output.outputFileName = "godot-lib.${outputSuffix}" } // Find scons' executable path @@ -88,13 +125,11 @@ android { for (ext in sconsExts) { String sconsNameExt = sconsName + ext logger.lifecycle("Checking $sconsNameExt") - sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt) if (sconsExecutableFile != null) { // We're done! break } - // Check all the options in path List<File> allOptions = org.gradle.internal.os.OperatingSystem.current().findAllInPath(sconsNameExt) if (!allOptions.isEmpty()) { @@ -103,27 +138,32 @@ android { break } } - 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}") } - // Creating gradle task to generate the native libraries for the default abi. - def taskName = getSconsTaskName(buildType) - tasks.create(name: taskName, type: Exec) { - executable sconsExecutableFile.absolutePath - args "--directory=${pathToRootDir}", "platform=android", "target=${releaseTarget}", "android_arch=${defaultAbi}", "-j" + Runtime.runtime.availableProcessors() - } + for (String selectedAbi : selectedAbis) { + if (!supportedAbis.contains(selectedAbi)) { + throw new GradleException("Invalid selected abi: $selectedAbi") + } - // Schedule the tasks so the generated libs are present before the aar file is packaged. - tasks["merge${buildType}JniLibFolders"].dependsOn taskName + // Creating gradle task to generate the native libraries for the selected abi. + def taskName = getSconsTaskName(flavorName, buildType, selectedAbi) + tasks.create(name: taskName, type: Exec) { + executable sconsExecutableFile.absolutePath + args "--directory=${pathToRootDir}", "platform=android", "tools=${toolsFlag}", "target=${sconsTarget}", "android_arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() + } + + // Schedule the tasks so the generated libs are present before the aar file is packaged. + tasks["merge${flavorName.capitalize()}${buildType.capitalize()}JniLibFolders"].dependsOn taskName + } } // TODO: Enable when issues with AGP 7.1+ are resolved (https://github.com/GodotVR/godot_openxr/issues/187). // publishing { -// singleVariant("release") { +// singleVariant("templateRelease") { // withSourcesJar() // withJavadocJar() // } 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 e8ffbb9481..fb1604f6af 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -65,10 +65,6 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God } else { Log.v(TAG, "Creating new Godot fragment instance."); godotFragment = initGodotInstance(); - if (godotFragment == null) { - throw new IllegalStateException("Godot instance must be non-null."); - } - getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss(); } } 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 78848c109a..377881be85 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -36,6 +36,7 @@ import static android.content.Context.WINDOW_SERVICE; import org.godotengine.godot.input.GodotEditText; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; +import org.godotengine.godot.tts.GodotTTS; import org.godotengine.godot.utils.GodotNetUtils; import org.godotengine.godot.utils.PermissionsUtil; import org.godotengine.godot.xr.XRMode; @@ -47,7 +48,6 @@ import android.app.AlertDialog; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipboardManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -166,6 +166,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC public static GodotIO io; public static GodotNetUtils netUtils; + public static GodotTTS tts; public interface ResultCallback { void callback(int requestCode, int resultCode, Intent data); @@ -333,9 +334,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } public void restart() { - if (godotHost != null) { - godotHost.onGodotRestartRequested(this); - } + runOnUiThread(() -> { + if (godotHost != null) { + godotHost.onGodotRestartRequested(this); + } + }); } public void alert(final String message, final String title) { @@ -457,6 +460,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC io = new GodotIO(activity); GodotLib.io = io; netUtils = new GodotNetUtils(activity); + tts = new GodotTTS(activity); mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); @@ -508,17 +512,14 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC use_debug_opengl = true; } else if (command_line[i].equals("--use_immersive")) { use_immersive = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ - window.getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar - View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - - UiChangeListener(); - } + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar + View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + UiChangeListener(); } else if (command_line[i].equals("--use_apk_expansion")) { use_apk_expansion = true; } else if (has_extra && command_line[i].equals("--apk_expansion_md5")) { @@ -665,14 +666,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } public String getClipboard() { - String copiedText = ""; - - if (mClipboard.hasPrimaryClip()) { - ClipData.Item item = mClipboard.getPrimaryClip().getItemAt(0); - copiedText = item.getText().toString(); - } - - return copiedText; + ClipData clipData = mClipboard.getPrimaryClip(); + if (clipData == null) + return ""; + CharSequence text = clipData.getItemAt(0).getText(); + if (text == null) + return ""; + return text.toString(); } public void setClipboard(String p_text) { @@ -698,7 +698,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); - if (use_immersive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ + if (use_immersive) { Window window = getActivity().getWindow(); window.getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | @@ -718,15 +718,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final View decorView = getActivity().getWindow().getDecorView(); decorView.setOnSystemUiVisibilityChangeListener(visibility -> { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - decorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); } }); } @@ -859,9 +857,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private void forceQuit() { // TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each // native Godot components that is started in Godot#onVideoInit. - if (godotHost != null) { - godotHost.onGodotForceQuit(this); - } + runOnUiThread(() -> { + if (godotHost != null) { + godotHost.onGodotForceQuit(this); + } + }); } private boolean obbIsCorrupted(String f, String main_pack_md5) { @@ -885,9 +885,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC // Create Hex String StringBuilder hexString = new StringBuilder(); - for (int i = 0; i < messageDigest.length; i++) { - String s = Integer.toHexString(0xFF & messageDigest[i]); - + for (byte b : messageDigest) { + String s = Integer.toHexString(0xFF & b); if (s.length() == 1) { s = "0" + s; } @@ -1010,6 +1009,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal)); } + public void initInputDevices() { mRenderView.initInputDevices(); } @@ -1018,4 +1018,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private GodotRenderView getRenderView() { // used by native side to get renderView return mRenderView; } + + @Keep + private void createNewGodotInstance(String[] args) { + runOnUiThread(() -> { + if (godotHost != null) { + godotHost.onNewGodotInstanceRequested(args); + } + }); + } } 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 61093d54de..08da1b1832 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -29,6 +29,8 @@ /*************************************************************************/ package org.godotengine.godot; +import org.godotengine.godot.gl.GLSurfaceView; +import org.godotengine.godot.gl.GodotRenderer; import org.godotengine.godot.input.GodotGestureHandler; import org.godotengine.godot.input.GodotInputHandler; import org.godotengine.godot.utils.GLUtils; @@ -43,7 +45,6 @@ import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PixelFormat; -import android.opengl.GLSurfaceView; import android.os.Build; import android.view.GestureDetector; import android.view.KeyEvent; diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java index 8e8f993369..2e7b67194f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java @@ -60,8 +60,16 @@ public interface GodotHost { default void onGodotForceQuit(Godot instance) {} /** - * Invoked on the GL thread when the Godot instance wants to be restarted. It's up to the host + * Invoked on the UI thread when the Godot instance wants to be restarted. It's up to the host * to perform the appropriate action(s). */ default void onGodotRestartRequested(Godot instance) {} + + /** + * Invoked on the UI thread when a new Godot instance is requested. It's up to the host to + * perform the appropriate action(s). + * + * @param args Arguments used to initialize the new instance. + */ + default void onNewGodotInstanceRequested(String[] args) {} } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index b151e7eec1..e8e292df5d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -222,10 +222,14 @@ public class GodotIO { } public int getScreenDPI() { - DisplayMetrics metrics = activity.getApplicationContext().getResources().getDisplayMetrics(); + DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); return (int)(metrics.density * 160f); } + public float getScaledDensity() { + return activity.getResources().getDisplayMetrics().scaledDensity; + } + public double getScreenRefreshRate(double fallback) { Display display = activity.getWindowManager().getDefaultDisplay(); if (display != null) { 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 29e4b4b29e..1f8f8c82a6 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -30,6 +30,8 @@ package org.godotengine.godot; +import org.godotengine.godot.gl.GodotRenderer; + import android.app.Activity; import android.hardware.SensorEvent; import android.view.Surface; @@ -68,7 +70,7 @@ public class GodotLib { * @param p_surface * @param p_width * @param p_height - * @see android.opengl.GLSurfaceView.Renderer#onSurfaceChanged(GL10, int, int) + * @see org.godotengine.godot.gl.GLSurfaceView.Renderer#onSurfaceChanged(GL10, int, int) */ public static native void resize(Surface p_surface, int p_width, int p_height); @@ -85,9 +87,14 @@ public class GodotLib { /** * Invoked on the GL thread to draw the current frame. - * @see android.opengl.GLSurfaceView.Renderer#onDrawFrame(GL10) + * @see org.godotengine.godot.gl.GLSurfaceView.Renderer#onDrawFrame(GL10) + */ + public static native boolean step(); + + /** + * TTS callback. */ - public static native void step(); + public static native void ttsCallback(int event, int id, int pos); /** * Forward touch events from the main thread to the GL thread. diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper.java new file mode 100644 index 0000000000..af16cfce74 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper.java @@ -0,0 +1,566 @@ +// clang-format off + +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.godotengine.godot.gl; + +import android.opengl.GLDebugHelper; +import android.opengl.GLException; + +import java.io.IOException; +import java.io.Writer; + +import javax.microedition.khronos.egl.EGL; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; + +class EGLLogWrapper implements EGL11 { + private EGL10 mEgl10; + Writer mLog; + boolean mLogArgumentNames; + boolean mCheckError; + private int mArgCount; + + + public EGLLogWrapper(EGL egl, int configFlags, Writer log) { + mEgl10 = (EGL10) egl; + mLog = log; + mLogArgumentNames = + (GLDebugHelper.CONFIG_LOG_ARGUMENT_NAMES & configFlags) != 0; + mCheckError = + (GLDebugHelper.CONFIG_CHECK_GL_ERROR & configFlags) != 0; + } + + public boolean eglChooseConfig(EGLDisplay display, int[] attrib_list, + EGLConfig[] configs, int config_size, int[] num_config) { + begin("eglChooseConfig"); + arg("display", display); + arg("attrib_list", attrib_list); + arg("config_size", config_size); + end(); + + boolean result = mEgl10.eglChooseConfig(display, attrib_list, configs, + config_size, num_config); + arg("configs", configs); + arg("num_config", num_config); + returns(result); + checkError(); + return result; + } + + public boolean eglCopyBuffers(EGLDisplay display, EGLSurface surface, + Object native_pixmap) { + begin("eglCopyBuffers"); + arg("display", display); + arg("surface", surface); + arg("native_pixmap", native_pixmap); + end(); + + boolean result = mEgl10.eglCopyBuffers(display, surface, native_pixmap); + returns(result); + checkError(); + return result; + } + + public EGLContext eglCreateContext(EGLDisplay display, EGLConfig config, + EGLContext share_context, int[] attrib_list) { + begin("eglCreateContext"); + arg("display", display); + arg("config", config); + arg("share_context", share_context); + arg("attrib_list", attrib_list); + end(); + + EGLContext result = mEgl10.eglCreateContext(display, config, + share_context, attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreatePbufferSurface(EGLDisplay display, + EGLConfig config, int[] attrib_list) { + begin("eglCreatePbufferSurface"); + arg("display", display); + arg("config", config); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = mEgl10.eglCreatePbufferSurface(display, config, + attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreatePixmapSurface(EGLDisplay display, + EGLConfig config, Object native_pixmap, int[] attrib_list) { + begin("eglCreatePixmapSurface"); + arg("display", display); + arg("config", config); + arg("native_pixmap", native_pixmap); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = mEgl10.eglCreatePixmapSurface(display, config, + native_pixmap, attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreateWindowSurface(EGLDisplay display, + EGLConfig config, Object native_window, int[] attrib_list) { + begin("eglCreateWindowSurface"); + arg("display", display); + arg("config", config); + arg("native_window", native_window); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = mEgl10.eglCreateWindowSurface(display, config, + native_window, attrib_list); + returns(result); + checkError(); + return result; + } + + public boolean eglDestroyContext(EGLDisplay display, EGLContext context) { + begin("eglDestroyContext"); + arg("display", display); + arg("context", context); + end(); + + boolean result = mEgl10.eglDestroyContext(display, context); + returns(result); + checkError(); + return result; + } + + public boolean eglDestroySurface(EGLDisplay display, EGLSurface surface) { + begin("eglDestroySurface"); + arg("display", display); + arg("surface", surface); + end(); + + boolean result = mEgl10.eglDestroySurface(display, surface); + returns(result); + checkError(); + return result; + } + + public boolean eglGetConfigAttrib(EGLDisplay display, EGLConfig config, + int attribute, int[] value) { + begin("eglGetConfigAttrib"); + arg("display", display); + arg("config", config); + arg("attribute", attribute); + end(); + boolean result = mEgl10.eglGetConfigAttrib(display, config, attribute, + value); + arg("value", value); + returns(result); + checkError(); + return false; + } + + public boolean eglGetConfigs(EGLDisplay display, EGLConfig[] configs, + int config_size, int[] num_config) { + begin("eglGetConfigs"); + arg("display", display); + arg("config_size", config_size); + end(); + + boolean result = mEgl10.eglGetConfigs(display, configs, config_size, + num_config); + arg("configs", configs); + arg("num_config", num_config); + returns(result); + checkError(); + return result; + } + + public EGLContext eglGetCurrentContext() { + begin("eglGetCurrentContext"); + end(); + + EGLContext result = mEgl10.eglGetCurrentContext(); + returns(result); + + checkError(); + return result; + } + + public EGLDisplay eglGetCurrentDisplay() { + begin("eglGetCurrentDisplay"); + end(); + + EGLDisplay result = mEgl10.eglGetCurrentDisplay(); + returns(result); + + checkError(); + return result; + } + + public EGLSurface eglGetCurrentSurface(int readdraw) { + begin("eglGetCurrentSurface"); + arg("readdraw", readdraw); + end(); + + EGLSurface result = mEgl10.eglGetCurrentSurface(readdraw); + returns(result); + + checkError(); + return result; + } + + public EGLDisplay eglGetDisplay(Object native_display) { + begin("eglGetDisplay"); + arg("native_display", native_display); + end(); + + EGLDisplay result = mEgl10.eglGetDisplay(native_display); + returns(result); + + checkError(); + return result; + } + + public int eglGetError() { + begin("eglGetError"); + end(); + + int result = mEgl10.eglGetError(); + returns(getErrorString(result)); + + return result; + } + + public boolean eglInitialize(EGLDisplay display, int[] major_minor) { + begin("eglInitialize"); + arg("display", display); + end(); + boolean result = mEgl10.eglInitialize(display, major_minor); + returns(result); + arg("major_minor", major_minor); + checkError(); + return result; + } + + public boolean eglMakeCurrent(EGLDisplay display, EGLSurface draw, + EGLSurface read, EGLContext context) { + begin("eglMakeCurrent"); + arg("display", display); + arg("draw", draw); + arg("read", read); + arg("context", context); + end(); + boolean result = mEgl10.eglMakeCurrent(display, draw, read, context); + returns(result); + checkError(); + return result; + } + + public boolean eglQueryContext(EGLDisplay display, EGLContext context, + int attribute, int[] value) { + begin("eglQueryContext"); + arg("display", display); + arg("context", context); + arg("attribute", attribute); + end(); + boolean result = mEgl10.eglQueryContext(display, context, attribute, + value); + returns(value[0]); + returns(result); + checkError(); + return result; + } + + public String eglQueryString(EGLDisplay display, int name) { + begin("eglQueryString"); + arg("display", display); + arg("name", name); + end(); + String result = mEgl10.eglQueryString(display, name); + returns(result); + checkError(); + return result; + } + + public boolean eglQuerySurface(EGLDisplay display, EGLSurface surface, + int attribute, int[] value) { + begin("eglQuerySurface"); + arg("display", display); + arg("surface", surface); + arg("attribute", attribute); + end(); + boolean result = mEgl10.eglQuerySurface(display, surface, attribute, + value); + returns(value[0]); + returns(result); + checkError(); + return result; + } + + public boolean eglSwapBuffers(EGLDisplay display, EGLSurface surface) { + begin("eglSwapBuffers"); + arg("display", display); + arg("surface", surface); + end(); + boolean result = mEgl10.eglSwapBuffers(display, surface); + returns(result); + checkError(); + return result; + } + + public boolean eglTerminate(EGLDisplay display) { + begin("eglTerminate"); + arg("display", display); + end(); + boolean result = mEgl10.eglTerminate(display); + returns(result); + checkError(); + return result; + } + + public boolean eglWaitGL() { + begin("eglWaitGL"); + end(); + boolean result = mEgl10.eglWaitGL(); + returns(result); + checkError(); + return result; + } + + public boolean eglWaitNative(int engine, Object bindTarget) { + begin("eglWaitNative"); + arg("engine", engine); + arg("bindTarget", bindTarget); + end(); + boolean result = mEgl10.eglWaitNative(engine, bindTarget); + returns(result); + checkError(); + return result; + } + + private void checkError() { + int eglError; + if ((eglError = mEgl10.eglGetError()) != EGL_SUCCESS) { + String errorMessage = "eglError: " + getErrorString(eglError); + logLine(errorMessage); + if (mCheckError) { + throw new GLException(eglError, errorMessage); + } + } + } + + private void logLine(String message) { + log(message + '\n'); + } + + private void log(String message) { + try { + mLog.write(message); + } catch (IOException e) { + // Ignore exception, keep on trying + } + } + + private void begin(String name) { + log(name + '('); + mArgCount = 0; + } + + private void arg(String name, String value) { + if (mArgCount++ > 0) { + log(", "); + } + if (mLogArgumentNames) { + log(name + "="); + } + log(value); + } + + private void end() { + log(");\n"); + flush(); + } + + private void flush() { + try { + mLog.flush(); + } catch (IOException e) { + mLog = null; + } + } + + private void arg(String name, int value) { + arg(name, Integer.toString(value)); + } + + private void arg(String name, Object object) { + arg(name, toString(object)); + } + + private void arg(String name, EGLDisplay object) { + if (object == EGL10.EGL_DEFAULT_DISPLAY) { + arg(name, "EGL10.EGL_DEFAULT_DISPLAY"); + } else if (object == EGL_NO_DISPLAY) { + arg(name, "EGL10.EGL_NO_DISPLAY"); + } else { + arg(name, toString(object)); + } + } + + private void arg(String name, EGLContext object) { + if (object == EGL10.EGL_NO_CONTEXT) { + arg(name, "EGL10.EGL_NO_CONTEXT"); + } else { + arg(name, toString(object)); + } + } + + private void arg(String name, EGLSurface object) { + if (object == EGL10.EGL_NO_SURFACE) { + arg(name, "EGL10.EGL_NO_SURFACE"); + } else { + arg(name, toString(object)); + } + } + + private void returns(String result) { + log(" returns " + result + ";\n"); + flush(); + } + + private void returns(int result) { + returns(Integer.toString(result)); + } + + private void returns(boolean result) { + returns(Boolean.toString(result)); + } + + private void returns(Object result) { + returns(toString(result)); + } + + private String toString(Object obj) { + if (obj == null) { + return "null"; + } else { + return obj.toString(); + } + } + + private void arg(String name, int[] arr) { + if (arr == null) { + arg(name, "null"); + } else { + arg(name, toString(arr.length, arr, 0)); + } + } + + private void arg(String name, Object[] arr) { + if (arr == null) { + arg(name, "null"); + } else { + arg(name, toString(arr.length, arr, 0)); + } + } + + private String toString(int n, int[] arr, int offset) { + StringBuilder buf = new StringBuilder(); + buf.append("{\n"); + int arrLen = arr.length; + for (int i = 0; i < n; i++) { + int index = offset + i; + buf.append(" [" + index + "] = "); + if (index < 0 || index >= arrLen) { + buf.append("out of bounds"); + } else { + buf.append(arr[index]); + } + buf.append('\n'); + } + buf.append("}"); + return buf.toString(); + } + + private String toString(int n, Object[] arr, int offset) { + StringBuilder buf = new StringBuilder(); + buf.append("{\n"); + int arrLen = arr.length; + for (int i = 0; i < n; i++) { + int index = offset + i; + buf.append(" [" + index + "] = "); + if (index < 0 || index >= arrLen) { + buf.append("out of bounds"); + } else { + buf.append(arr[index]); + } + buf.append('\n'); + } + buf.append("}"); + return buf.toString(); + } + + private static String getHex(int value) { + return "0x" + Integer.toHexString(value); + } + + public static String getErrorString(int error) { + switch (error) { + case EGL_SUCCESS: + return "EGL_SUCCESS"; + case EGL_NOT_INITIALIZED: + return "EGL_NOT_INITIALIZED"; + case EGL_BAD_ACCESS: + return "EGL_BAD_ACCESS"; + case EGL_BAD_ALLOC: + return "EGL_BAD_ALLOC"; + case EGL_BAD_ATTRIBUTE: + return "EGL_BAD_ATTRIBUTE"; + case EGL_BAD_CONFIG: + return "EGL_BAD_CONFIG"; + case EGL_BAD_CONTEXT: + return "EGL_BAD_CONTEXT"; + case EGL_BAD_CURRENT_SURFACE: + return "EGL_BAD_CURRENT_SURFACE"; + case EGL_BAD_DISPLAY: + return "EGL_BAD_DISPLAY"; + case EGL_BAD_MATCH: + return "EGL_BAD_MATCH"; + case EGL_BAD_NATIVE_PIXMAP: + return "EGL_BAD_NATIVE_PIXMAP"; + case EGL_BAD_NATIVE_WINDOW: + return "EGL_BAD_NATIVE_WINDOW"; + case EGL_BAD_PARAMETER: + return "EGL_BAD_PARAMETER"; + case EGL_BAD_SURFACE: + return "EGL_BAD_SURFACE"; + case EGL11.EGL_CONTEXT_LOST: + return "EGL_CONTEXT_LOST"; + default: + return getHex(error); + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java new file mode 100644 index 0000000000..8449c08b88 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java @@ -0,0 +1,1939 @@ +// clang-format off + +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.godotengine.godot.gl; + +import android.content.Context; +import android.opengl.EGL14; +import android.opengl.EGLExt; +import android.opengl.GLDebugHelper; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import java.io.Writer; +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; + +/** + * An implementation of SurfaceView that uses the dedicated surface for + * displaying OpenGL rendering. + * <p> + * A GLSurfaceView provides the following features: + * <p> + * <ul> + * <li>Manages a surface, which is a special piece of memory that can be + * composited into the Android view system. + * <li>Manages an EGL display, which enables OpenGL to render into a surface. + * <li>Accepts a user-provided Renderer object that does the actual rendering. + * <li>Renders on a dedicated thread to decouple rendering performance from the + * UI thread. + * <li>Supports both on-demand and continuous rendering. + * <li>Optionally wraps, traces, and/or error-checks the renderer's OpenGL calls. + * </ul> + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about how to use OpenGL, read the + * <a href="{@docRoot}guide/topics/graphics/opengl.html">OpenGL</a> developer guide.</p> + * </div> + * + * <h3>Using GLSurfaceView</h3> + * <p> + * Typically you use GLSurfaceView by subclassing it and overriding one or more of the + * View system input event methods. If your application does not need to override event + * methods then GLSurfaceView can be used as-is. For the most part + * GLSurfaceView behavior is customized by calling "set" methods rather than by subclassing. + * For example, unlike a regular View, drawing is delegated to a separate Renderer object which + * is registered with the GLSurfaceView + * using the {@link #setRenderer(Renderer)} call. + * <p> + * <h3>Initializing GLSurfaceView</h3> + * All you have to do to initialize a GLSurfaceView is call {@link #setRenderer(Renderer)}. + * However, if desired, you can modify the default behavior of GLSurfaceView by calling one or + * more of these methods before calling setRenderer: + * <ul> + * <li>{@link #setDebugFlags(int)} + * <li>{@link #setEGLConfigChooser(boolean)} + * <li>{@link #setEGLConfigChooser(EGLConfigChooser)} + * <li>{@link #setEGLConfigChooser(int, int, int, int, int, int)} + * <li>{@link #setGLWrapper(GLWrapper)} + * </ul> + * <p> + * <h4>Specifying the android.view.Surface</h4> + * By default GLSurfaceView will create a PixelFormat.RGB_888 format surface. If a translucent + * surface is required, call getHolder().setFormat(PixelFormat.TRANSLUCENT). + * The exact format of a TRANSLUCENT surface is device dependent, but it will be + * a 32-bit-per-pixel surface with 8 bits per component. + * <p> + * <h4>Choosing an EGL Configuration</h4> + * A given Android device may support multiple EGLConfig rendering configurations. + * The available configurations may differ in how many channels of data are present, as + * well as how many bits are allocated to each channel. Therefore, the first thing + * GLSurfaceView has to do when starting to render is choose what EGLConfig to use. + * <p> + * By default GLSurfaceView chooses a EGLConfig that has an RGB_888 pixel format, + * with at least a 16-bit depth buffer and no stencil. + * <p> + * If you would prefer a different EGLConfig + * you can override the default behavior by calling one of the + * setEGLConfigChooser methods. + * <p> + * <h4>Debug Behavior</h4> + * You can optionally modify the behavior of GLSurfaceView by calling + * one or more of the debugging methods {@link #setDebugFlags(int)}, + * and {@link #setGLWrapper}. These methods may be called before and/or after setRenderer, but + * typically they are called before setRenderer so that they take effect immediately. + * <p> + * <h4>Setting a Renderer</h4> + * Finally, you must call {@link #setRenderer} to register a {@link Renderer}. + * The renderer is + * responsible for doing the actual OpenGL rendering. + * <p> + * <h3>Rendering Mode</h3> + * Once the renderer is set, you can control whether the renderer draws + * continuously or on-demand by calling + * {@link #setRenderMode}. The default is continuous rendering. + * <p> + * <h3>Activity Life-cycle</h3> + * A GLSurfaceView must be notified when to pause and resume rendering. GLSurfaceView clients + * are required to call {@link #onPause()} when the activity stops and + * {@link #onResume()} when the activity starts. These calls allow GLSurfaceView to + * pause and resume the rendering thread, and also allow GLSurfaceView to release and recreate + * the OpenGL display. + * <p> + * <h3>Handling events</h3> + * <p> + * To handle an event you will typically subclass GLSurfaceView and override the + * appropriate method, just as you would with any other View. However, when handling + * the event, you may need to communicate with the Renderer object + * that's running in the rendering thread. You can do this using any + * standard Java cross-thread communication mechanism. In addition, + * one relatively easy way to communicate with your renderer is + * to call + * {@link #queueEvent(Runnable)}. For example: + * <pre class="prettyprint"> + * class MyGLSurfaceView extends GLSurfaceView { + * + * private MyRenderer mMyRenderer; + * + * public void start() { + * mMyRenderer = ...; + * setRenderer(mMyRenderer); + * } + * + * public boolean onKeyDown(int keyCode, KeyEvent event) { + * if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + * queueEvent(new Runnable() { + * // This method will be called on the rendering + * // thread: + * public void run() { + * mMyRenderer.handleDpadCenter(); + * }}); + * return true; + * } + * return super.onKeyDown(keyCode, event); + * } + * } + * </pre> + * + */ +public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback2 { + private final static String TAG = "GLSurfaceView"; + private final static boolean LOG_ATTACH_DETACH = false; + private final static boolean LOG_THREADS = false; + private final static boolean LOG_PAUSE_RESUME = false; + private final static boolean LOG_SURFACE = false; + private final static boolean LOG_RENDERER = false; + private final static boolean LOG_RENDERER_DRAW_FRAME = false; + private final static boolean LOG_EGL = false; + /** + * The renderer only renders + * when the surface is created, or when {@link #requestRender} is called. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + * @see #requestRender() + */ + public final static int RENDERMODE_WHEN_DIRTY = 0; + /** + * The renderer is called + * continuously to re-render the scene. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + */ + public final static int RENDERMODE_CONTINUOUSLY = 1; + + /** + * Check glError() after every GL call and throw an exception if glError indicates + * that an error has occurred. This can be used to help track down which OpenGL ES call + * is causing an error. + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_CHECK_GL_ERROR = 1; + + /** + * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView". + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_LOG_GL_CALLS = 2; + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLSurfaceView(Context context) { + super(context); + init(); + } + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + + private void init() { + // Install a SurfaceHolder.Callback so we get notified when the + // underlying surface is created and destroyed + SurfaceHolder holder = getHolder(); + holder.addCallback(this); + // setFormat is done by SurfaceView in SDK 2.3 and newer. Uncomment + // this statement if back-porting to 2.2 or older: + // holder.setFormat(PixelFormat.RGB_565); + // + // setType is not needed for SDK 2.0 or newer. Uncomment this + // statement if back-porting this code to older SDKs. + // holder.setType(SurfaceHolder.SURFACE_TYPE_GPU); + } + + /** + * Set the glWrapper. If the glWrapper is not null, its + * {@link GLWrapper#wrap(GL)} method is called + * whenever a surface is created. A GLWrapper can be used to wrap + * the GL object that's passed to the renderer. Wrapping a GL + * object enables examining and modifying the behavior of the + * GL calls made by the renderer. + * <p> + * Wrapping is typically used for debugging purposes. + * <p> + * The default value is null. + * @param glWrapper the new GLWrapper + */ + public void setGLWrapper(GLWrapper glWrapper) { + mGLWrapper = glWrapper; + } + + /** + * Set the debug flags to a new value. The value is + * constructed by OR-together zero or more + * of the DEBUG_CHECK_* constants. The debug flags take effect + * whenever a surface is created. The default value is zero. + * @param debugFlags the new debug flags + * @see #DEBUG_CHECK_GL_ERROR + * @see #DEBUG_LOG_GL_CALLS + */ + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + + /** + * Get the current value of the debug flags. + * @return the current value of the debug flags. + */ + public int getDebugFlags() { + return mDebugFlags; + } + + /** + * Control whether the EGL context is preserved when the GLSurfaceView is paused and + * resumed. + * <p> + * If set to true, then the EGL context may be preserved when the GLSurfaceView is paused. + * <p> + * Prior to API level 11, whether the EGL context is actually preserved or not + * depends upon whether the Android device can support an arbitrary number of + * EGL contexts or not. Devices that can only support a limited number of EGL + * contexts must release the EGL context in order to allow multiple applications + * to share the GPU. + * <p> + * If set to false, the EGL context will be released when the GLSurfaceView is paused, + * and recreated when the GLSurfaceView is resumed. + * <p> + * + * The default is false. + * + * @param preserveOnPause preserve the EGL context when paused + */ + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + mPreserveEGLContextOnPause = preserveOnPause; + } + + /** + * @return true if the EGL context will be preserved when paused + */ + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + + /** + * Set the renderer associated with this view. Also starts the thread that + * will call the renderer, which in turn causes the rendering to start. + * <p>This method should be called once and only once in the life-cycle of + * a GLSurfaceView. + * <p>The following GLSurfaceView methods can only be called <em>before</em> + * setRenderer is called: + * <ul> + * <li>{@link #setEGLConfigChooser(boolean)} + * <li>{@link #setEGLConfigChooser(EGLConfigChooser)} + * <li>{@link #setEGLConfigChooser(int, int, int, int, int, int)} + * </ul> + * <p> + * The following GLSurfaceView methods can only be called <em>after</em> + * setRenderer is called: + * <ul> + * <li>{@link #getRenderMode()} + * <li>{@link #onPause()} + * <li>{@link #onResume()} + * <li>{@link #queueEvent(Runnable)} + * <li>{@link #requestRender()} + * <li>{@link #setRenderMode(int)} + * </ul> + * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + public void setRenderer(Renderer renderer) { + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + + /** + * Install a custom EGLContextFactory. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If this method is not called, then by default + * a context will be created with no shared context and + * with a null attribute list. + */ + public void setEGLContextFactory(EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + + /** + * Install a custom EGLWindowSurfaceFactory. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If this method is not called, then by default + * a window surface will be created with a null attribute list. + */ + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + + /** + * Install a custom EGLConfigChooser. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If no setEGLConfigChooser method is called, then by default the + * view will choose an EGLConfig that is compatible with the current + * android.view.Surface, with a depth buffer depth of + * at least 16 bits. + * @param configChooser + */ + public void setEGLConfigChooser(EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + + /** + * Install a config chooser which will choose a config + * as close to 16-bit RGB as possible, with or without an optional depth + * buffer as close to 16-bits as possible. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + * @param needDepth + */ + public void setEGLConfigChooser(boolean needDepth) { + setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); + } + + /** + * Install a config chooser which will choose a config + * with at least the specified depthSize and stencilSize, + * and exactly the specified redSize, greenSize, blueSize and alphaSize. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + */ + public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, + blueSize, alphaSize, depthSize, stencilSize)); + } + + /** + * Inform the default EGLContextFactory and default EGLConfigChooser + * which EGLContext client version to pick. + * <p>Use this method to create an OpenGL ES 2.0-compatible context. + * Example: + * <pre class="prettyprint"> + * public MyView(Context context) { + * super(context); + * setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context. + * setRenderer(new MyRenderer()); + * } + * </pre> + * <p>Note: Activities which require OpenGL ES 2.0 should indicate this by + * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's + * AndroidManifest.xml file. + * <p>If this method is called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p>This method only affects the behavior of the default EGLContexFactory and the + * default EGLConfigChooser. If + * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied + * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. + * If + * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied + * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. + * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 + */ + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + + /** + * Set the rendering mode. When renderMode is + * RENDERMODE_CONTINUOUSLY, the renderer is called + * repeatedly to re-render the scene. When renderMode + * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface + * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. + * <p> + * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance + * by allowing the GPU and CPU to idle when the view does not need to be updated. + * <p> + * This method can only be called after {@link #setRenderer(Renderer)} + * + * @param renderMode one of the RENDERMODE_X constants + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public void setRenderMode(int renderMode) { + mGLThread.setRenderMode(renderMode); + } + + /** + * Get the current rendering mode. May be called + * from any thread. Must not be called before a renderer has been set. + * @return the current rendering mode. + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. + * May be called + * from any thread. Must not be called before a renderer has been set. + */ + public void requestRender() { + mGLThread.requestRender(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceCreated(SurfaceHolder holder) { + mGLThread.surfaceCreated(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceDestroyed(SurfaceHolder holder) { + // Surface will be destroyed when we return + mGLThread.surfaceDestroyed(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + mGLThread.onWindowResize(w, h); + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void surfaceRedrawNeededAsync(SurfaceHolder holder, Runnable finishDrawing) { + if (mGLThread != null) { + mGLThread.requestRenderAndNotify(finishDrawing); + } + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Deprecated + @Override + public void surfaceRedrawNeeded(SurfaceHolder holder) { + // Since we are part of the framework we know only surfaceRedrawNeededAsync + // will be called. + } + + + /** + * Pause the rendering thread, optionally tearing down the EGL context + * depending upon the value of {@link #setPreserveEGLContextOnPause(boolean)}. + * + * This method should be called when it is no longer desirable for the + * GLSurfaceView to continue rendering, such as in response to + * {@link android.app.Activity#onStop Activity.onStop}. + * + * Must not be called before a renderer has been set. + */ + public void onPause() { + mGLThread.onPause(); + } + + /** + * Resumes the rendering thread, re-creating the OpenGL context if necessary. It + * is the counterpart to {@link #onPause()}. + * + * This method should typically be called in + * {@link android.app.Activity#onStart Activity.onStart}. + * + * Must not be called before a renderer has been set. + */ + public void onResume() { + mGLThread.onResume(); + } + + /** + * Queue a runnable to be run on the GL rendering thread. This can be used + * to communicate with the Renderer on the rendering thread. + * Must not be called before a renderer has been set. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + mGLThread.queueEvent(r); + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLSurfaceView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + mGLThread.start(); + } + mDetached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + + // ---------------------------------------------------------------------- + + /** + * An interface used to wrap a GL interface. + * <p>Typically + * used for implementing debugging and tracing on top of the default + * GL interface. You would typically use this by creating your own class + * that implemented all the GL methods by delegating to another GL instance. + * Then you could add your own behavior before or after calling the + * delegate. All the GLWrapper would do was instantiate and return the + * wrapper GL instance: + * <pre class="prettyprint"> + * class MyGLWrapper implements GLWrapper { + * GL wrap(GL gl) { + * return new MyGLImplementation(gl); + * } + * static class MyGLImplementation implements GL,GL10,GL11,... { + * ... + * } + * } + * </pre> + * @see #setGLWrapper(GLWrapper) + */ + public interface GLWrapper { + /** + * Wraps a gl interface in another gl interface. + * @param gl a GL interface that is to be wrapped. + * @return either the input argument or another GL object that wraps the input argument. + */ + GL wrap(GL gl); + } + + /** + * A generic renderer interface. + * <p> + * The renderer is responsible for making OpenGL calls to render a frame. + * <p> + * GLSurfaceView clients typically create their own classes that implement + * this interface, and then call {@link GLSurfaceView#setRenderer} to + * register the renderer with the GLSurfaceView. + * <p> + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about how to use OpenGL, read the + * <a href="{@docRoot}guide/topics/graphics/opengl.html">OpenGL</a> developer guide.</p> + * </div> + * + * <h3>Threading</h3> + * The renderer will be called on a separate thread, so that rendering + * performance is decoupled from the UI thread. Clients typically need to + * communicate with the renderer from the UI thread, because that's where + * input events are received. Clients can communicate using any of the + * standard Java techniques for cross-thread communication, or they can + * use the {@link GLSurfaceView#queueEvent(Runnable)} convenience method. + * <p> + * <h3>EGL Context Lost</h3> + * There are situations where the EGL rendering context will be lost. This + * typically happens when device wakes up after going to sleep. When + * the EGL context is lost, all OpenGL resources (such as textures) that are + * associated with that context will be automatically deleted. In order to + * keep rendering correctly, a renderer must recreate any lost resources + * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method + * is a convenient place to do this. + * + * + * @see #setRenderer(Renderer) + */ + public interface Renderer { + /** + * Called when the surface is created or recreated. + * <p> + * Called when the rendering thread + * starts and whenever the EGL context is lost. The EGL context will typically + * be lost when the Android device awakes after going to sleep. + * <p> + * Since this method is called at the beginning of rendering, as well as + * every time the EGL context is lost, this method is a convenient place to put + * code to create resources that need to be created when the rendering + * starts, and that need to be recreated when the EGL context is lost. + * Textures are an example of a resource that you might want to create + * here. + * <p> + * Note that when the EGL context is lost, all OpenGL resources associated + * with that context will be automatically deleted. You do not need to call + * the corresponding "glDelete" methods such as glDeleteTextures to + * manually delete these lost resources. + * <p> + * @param gl the GL interface. Use <code>instanceof</code> to + * test if the interface supports GL11 or higher interfaces. + * @param config the EGLConfig of the created surface. Can be used + * to create matching pbuffers. + */ + void onSurfaceCreated(GL10 gl, EGLConfig config); + + /** + * Called when the surface changed size. + * <p> + * Called after the surface is created and whenever + * the OpenGL ES surface size changes. + * <p> + * Typically you will set your viewport here. If your camera + * is fixed then you could also set your projection matrix here: + * <pre class="prettyprint"> + * void onSurfaceChanged(GL10 gl, int width, int height) { + * gl.glViewport(0, 0, width, height); + * // for a fixed camera, set the projection too + * float ratio = (float) width / height; + * gl.glMatrixMode(GL10.GL_PROJECTION); + * gl.glLoadIdentity(); + * gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); + * } + * </pre> + * @param gl the GL interface. Use <code>instanceof</code> to + * test if the interface supports GL11 or higher interfaces. + * @param width + * @param height + */ + void onSurfaceChanged(GL10 gl, int width, int height); + + // -- GODOT start -- + /** + * Called to draw the current frame. + * <p> + * This method is responsible for drawing the current frame. + * <p> + * The implementation of this method typically looks like this: + * <pre class="prettyprint"> + * boolean onDrawFrame(GL10 gl) { + * gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); + * //... other gl calls to render the scene ... + * return true; + * } + * </pre> + * @param gl the GL interface. Use <code>instanceof</code> to + * test if the interface supports GL11 or higher interfaces. + * + * @return true if the buffers should be swapped, false otherwise. + */ + boolean onDrawFrame(GL10 gl); + // -- GODOT end -- + } + + /** + * An interface for customizing the eglCreateContext and eglDestroyContext calls. + * <p> + * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLContextFactory(EGLContextFactory)} + */ + public interface EGLContextFactory { + EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig); + void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context); + } + + private class DefaultContextFactory implements EGLContextFactory { + private int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, + EGL10.EGL_NONE }; + + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, + mEGLContextClientVersion != 0 ? attrib_list : null); + } + + public void destroyContext(EGL10 egl, EGLDisplay display, + EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + if (LOG_THREADS) { + Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); + } + EglHelper.throwEglException("eglDestroyContex", egl.eglGetError()); + } + } + } + + /** + * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. + * <p> + * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} + */ + public interface EGLWindowSurfaceFactory { + /** + * @return null if the surface cannot be constructed. + */ + EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, + Object nativeWindow); + void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); + } + + private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { + + public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, + EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e(TAG, "eglCreateWindowSurface", e); + } + return result; + } + + public void destroySurface(EGL10 egl, EGLDisplay display, + EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + } + + /** + * An interface for choosing an EGLConfig configuration from a list of + * potential configurations. + * <p> + * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLConfigChooser(EGLConfigChooser)} + */ + public interface EGLConfigChooser { + /** + * Choose a configuration from the list. Implementors typically + * implement this method by calling + * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the + * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. + * @param egl the EGL10 for the current display. + * @param display the current display. + * @return the chosen configuration. + */ + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); + } + + private abstract class BaseConfigChooser + implements EGLConfigChooser { + public BaseConfigChooser(int[] configSpec) { + mConfigSpec = filterConfigSpec(configSpec); + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] num_config = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException( + "No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs); + + protected int[] mConfigSpec; + + private int[] filterConfigSpec(int[] configSpec) { + if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); + newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE; + if (mEGLContextClientVersion == 2) { + newConfigSpec[len] = EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */ + } else { + newConfigSpec[len] = EGLExt.EGL_OPENGL_ES3_BIT_KHR; /* EGL_OPENGL_ES3_BIT_KHR */ + } + newConfigSpec[len+1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + private class ComponentSizeChooser extends BaseConfigChooser { + public ComponentSizeChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(new int[] { + EGL10.EGL_RED_SIZE, redSize, + EGL10.EGL_GREEN_SIZE, greenSize, + EGL10.EGL_BLUE_SIZE, blueSize, + EGL10.EGL_ALPHA_SIZE, alphaSize, + EGL10.EGL_DEPTH_SIZE, depthSize, + EGL10.EGL_STENCIL_SIZE, stencilSize, + EGL10.EGL_NONE}); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + if ((d >= mDepthSize) && (s >= mStencilSize)) { + int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + if ((r == mRedSize) && (g == mGreenSize) + && (b == mBlueSize) && (a == mAlphaSize)) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + } + + /** + * This class will choose a RGB_888 surface with + * or without a depth buffer. + * + */ + private class SimpleEGLConfigChooser extends ComponentSizeChooser { + public SimpleEGLConfigChooser(boolean withDepthBuffer) { + super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); + } + } + + /** + * An EGL helper class. + */ + + private static class EglHelper { + public EglHelper(WeakReference<GLSurfaceView> glSurfaceViewWeakRef) { + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + /** + * Initialize EGL for a given configuration spec. + */ + public void start() { + if (LOG_EGL) { + Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); + } + /* + * Get an EGL instance + */ + mEgl = (EGL10) EGLContext.getEGL(); + + /* + * Get to the default display. + */ + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + /* + * We can now initialize EGL for that display + */ + int[] version = new int[2]; + if(!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view == null) { + mEglConfig = null; + mEglContext = null; + } else { + mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); + } + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + mEglContext = null; + throwEglException("createContext"); + } + if (LOG_EGL) { + Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); + } + + mEglSurface = null; + } + + /** + * Create an egl surface for the current SurfaceHolder surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + public boolean createSurface() { + if (LOG_EGL) { + Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); + } + /* + * Check preconditions. + */ + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl, + mEglDisplay, mEglConfig, view.getHolder()); + } else { + mEglSurface = null; + } + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * SurfaceView surface has been destroyed. + */ + logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + * @return + */ + GL createGL() { + + GL gl = mEglContext.getGL(); + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + if (view.mGLWrapper != null) { + gl = view.mGLWrapper.wrap(gl); + } + + if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) { + int configFlags = 0; + Writer log = null; + if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { + configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; + } + if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { + log = new LogWriter(); + } + gl = GLDebugHelper.wrap(gl, configFlags, log); + } + } + return gl; + } + + /** + * Display the current render surface. + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + return mEgl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + public void destroySurface() { + if (LOG_EGL) { + Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); + } + destroySurfaceImp(); + } + + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + + public void finish() { + if (LOG_EGL) { + Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); + } + if (mEglContext != null) { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + private void throwEglException(String function) { + throwEglException(function, mEgl.eglGetError()); + } + + public static void throwEglException(String function, int error) { + String message = formatEglError(function, error); + if (LOG_THREADS) { + Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + + message); + } + throw new RuntimeException(message); + } + + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + public static String formatEglError(String function, int error) { + return function + " failed: " + EGLLogWrapper.getErrorString(error); + } + + private WeakReference<GLSurfaceView> mGLSurfaceViewWeakRef; + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + + } + + /** + * A generic GL Thread. Takes care of initializing EGL and GL. Delegates + * to a Renderer instance to do the actual drawing. Can be configured to + * render continuously or on request. + * + * All potentially blocking synchronization is done through the + * sGLThreadManager object. This avoids multiple-lock ordering issues. + * + */ + static class GLThread extends Thread { + GLThread(WeakReference<GLSurfaceView> glSurfaceViewWeakRef) { + super(); + mWidth = 0; + mHeight = 0; + mRequestRender = true; + mRenderMode = RENDERMODE_CONTINUOUSLY; + mWantRenderNotification = false; + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + @Override + public void run() { + setName("GLThread " + getId()); + if (LOG_THREADS) { + Log.i("GLThread", "starting tid=" + getId()); + } + + try { + guardedRun(); + } catch (InterruptedException e) { + // fall thru and exit normally + } finally { + sGLThreadManager.threadExiting(this); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (mHaveEglSurface) { + mHaveEglSurface = false; + mEglHelper.destroySurface(); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (mHaveEglContext) { + mEglHelper.finish(); + mHaveEglContext = false; + sGLThreadManager.releaseEglContextLocked(this); + } + } + private void guardedRun() throws InterruptedException { + mEglHelper = new EglHelper(mGLSurfaceViewWeakRef); + mHaveEglContext = false; + mHaveEglSurface = false; + mWantRenderNotification = false; + + try { + GL10 gl = null; + boolean createEglContext = false; + boolean createEglSurface = false; + boolean createGlInterface = false; + boolean lostEglContext = false; + boolean sizeChanged = false; + boolean wantRenderNotification = false; + boolean doRenderNotification = false; + boolean askedToReleaseEglContext = false; + int w = 0; + int h = 0; + Runnable event = null; + Runnable finishDrawingRunnable = null; + + while (true) { + synchronized (sGLThreadManager) { + while (true) { + if (mShouldExit) { + return; + } + + if (! mEventQueue.isEmpty()) { + event = mEventQueue.remove(0); + break; + } + + // Update the pause state. + boolean pausing = false; + if (mPaused != mRequestPaused) { + pausing = mRequestPaused; + mPaused = mRequestPaused; + sGLThreadManager.notifyAll(); + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); + } + } + + // Do we need to give up the EGL context? + if (mShouldReleaseEglContext) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); + } + stopEglSurfaceLocked(); + stopEglContextLocked(); + mShouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && mHaveEglSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); + } + stopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && mHaveEglContext) { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + boolean preserveEglContextOnPause = view == null ? + false : view.mPreserveEGLContextOnPause; + if (!preserveEglContextOnPause) { + stopEglContextLocked(); + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); + } + } + } + + // Have we lost the SurfaceView surface? + if ((! mHasSurface) && (! mWaitingForSurface)) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId()); + } + if (mHaveEglSurface) { + stopEglSurfaceLocked(); + } + mWaitingForSurface = true; + mSurfaceIsBad = false; + sGLThreadManager.notifyAll(); + } + + // Have we acquired the surface view surface? + if (mHasSurface && mWaitingForSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId()); + } + mWaitingForSurface = false; + sGLThreadManager.notifyAll(); + } + + if (doRenderNotification) { + if (LOG_SURFACE) { + Log.i("GLThread", "sending render notification tid=" + getId()); + } + mWantRenderNotification = false; + doRenderNotification = false; + mRenderComplete = true; + sGLThreadManager.notifyAll(); + } + + if (mFinishDrawingRunnable != null) { + finishDrawingRunnable = mFinishDrawingRunnable; + mFinishDrawingRunnable = null; + } + + // Ready to draw? + if (readyToDraw()) { + + // If we don't have an EGL context, try to acquire one. + if (! mHaveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else { + try { + mEglHelper.start(); + } catch (RuntimeException t) { + sGLThreadManager.releaseEglContextLocked(this); + throw t; + } + mHaveEglContext = true; + createEglContext = true; + + sGLThreadManager.notifyAll(); + } + } + + if (mHaveEglContext && !mHaveEglSurface) { + mHaveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (mHaveEglSurface) { + if (mSizeChanged) { + sizeChanged = true; + w = mWidth; + h = mHeight; + mWantRenderNotification = true; + if (LOG_SURFACE) { + Log.i("GLThread", + "noticing that we want render notification tid=" + + getId()); + } + + // Destroy and recreate the EGL surface. + createEglSurface = true; + + mSizeChanged = false; + } + mRequestRender = false; + sGLThreadManager.notifyAll(); + if (mWantRenderNotification) { + wantRenderNotification = true; + } + break; + } + } else { + if (finishDrawingRunnable != null) { + Log.w(TAG, "Warning, !readyToDraw() but waiting for " + + "draw finished! Early reporting draw finished."); + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } + // By design, this is the only place in a GLThread thread where we wait(). + if (LOG_THREADS) { + Log.i("GLThread", "waiting tid=" + getId() + + " mHaveEglContext: " + mHaveEglContext + + " mHaveEglSurface: " + mHaveEglSurface + + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface + + " mPaused: " + mPaused + + " mHasSurface: " + mHasSurface + + " mSurfaceIsBad: " + mSurfaceIsBad + + " mWaitingForSurface: " + mWaitingForSurface + + " mWidth: " + mWidth + + " mHeight: " + mHeight + + " mRequestRender: " + mRequestRender + + " mRenderMode: " + mRenderMode); + } + sGLThreadManager.wait(); + } + } // end of synchronized(sGLThreadManager) + + if (event != null) { + event.run(); + event = null; + continue; + } + + if (createEglSurface) { + if (LOG_SURFACE) { + Log.w("GLThread", "egl createSurface"); + } + if (mEglHelper.createSurface()) { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + sGLThreadManager.notifyAll(); + } + } else { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + } + + if (createGlInterface) { + gl = (GL10) mEglHelper.createGL(); + + createGlInterface = false; + } + + // -- GODOT start -- + if (createEglContext) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceCreated"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); + } finally { + } + } + createEglContext = false; + } + + if (sizeChanged) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + view.mRenderer.onSurfaceChanged(gl, w, h); + } finally { + } + } + sizeChanged = false; + } + + boolean swapBuffers = false; + if (LOG_RENDERER_DRAW_FRAME) { + Log.w("GLThread", "onDrawFrame tid=" + getId()); + } + { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + swapBuffers = view.mRenderer.onDrawFrame(gl); + if (finishDrawingRunnable != null) { + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } finally {} + } + } + if (swapBuffers) { + int swapError = mEglHelper.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + if (LOG_SURFACE) { + Log.i("GLThread", "egl context lost tid=" + getId()); + } + lostEglContext = true; + break; + default: + // Other errors typically mean that the current surface is bad, + // probably because the SurfaceView surface has been destroyed, + // but we haven't been notified yet. + // Log the error to help developers understand why rendering stopped. + EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError); + + synchronized (sGLThreadManager) { + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + break; + } + } + // -- GODOT end -- + + if (wantRenderNotification) { + doRenderNotification = true; + wantRenderNotification = false; + } + } + + } finally { + /* + * clean-up everything... + */ + synchronized (sGLThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + + public boolean ableToDraw() { + return mHaveEglContext && mHaveEglSurface && readyToDraw(); + } + + private boolean readyToDraw() { + return (!mPaused) && mHasSurface && (!mSurfaceIsBad) + && (mWidth > 0) && (mHeight > 0) + && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); + } + + public void setRenderMode(int renderMode) { + if ( !((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY)) ) { + throw new IllegalArgumentException("renderMode"); + } + synchronized(sGLThreadManager) { + mRenderMode = renderMode; + sGLThreadManager.notifyAll(); + } + } + + public int getRenderMode() { + synchronized(sGLThreadManager) { + return mRenderMode; + } + } + + public void requestRender() { + synchronized(sGLThreadManager) { + mRequestRender = true; + sGLThreadManager.notifyAll(); + } + } + + public void requestRenderAndNotify(Runnable finishDrawing) { + synchronized(sGLThreadManager) { + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We will return to the client rendering code, so here we don't need to + // do anything. + if (Thread.currentThread() == this) { + return; + } + + mWantRenderNotification = true; + mRequestRender = true; + mRenderComplete = false; + mFinishDrawingRunnable = finishDrawing; + + sGLThreadManager.notifyAll(); + } + } + + public void surfaceCreated() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceCreated tid=" + getId()); + } + mHasSurface = true; + mFinishedCreatingEglSurface = false; + sGLThreadManager.notifyAll(); + while (mWaitingForSurface + && !mFinishedCreatingEglSurface + && !mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceDestroyed() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceDestroyed tid=" + getId()); + } + mHasSurface = false; + sGLThreadManager.notifyAll(); + while((!mWaitingForSurface) && (!mExited)) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onPause() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onPause tid=" + getId()); + } + mRequestPaused = true; + sGLThreadManager.notifyAll(); + while ((! mExited) && (! mPaused)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onPause waiting for mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onResume() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onResume tid=" + getId()); + } + mRequestPaused = false; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + while ((! mExited) && mPaused && (!mRenderComplete)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onResume waiting for !mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onWindowResize(int w, int h) { + synchronized (sGLThreadManager) { + mWidth = w; + mHeight = h; + mSizeChanged = true; + mRequestRender = true; + mRenderComplete = false; + + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We need to process the size change eventually though and update our EGLSurface. + // So we set the parameters and return so they can be processed on our + // next iteration. + if (Thread.currentThread() == this) { + return; + } + + sGLThreadManager.notifyAll(); + + // Wait for thread to react to resize and render a frame + while (! mExited && !mPaused && !mRenderComplete + && ableToDraw()) { + if (LOG_SURFACE) { + Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized(sGLThreadManager) { + mShouldExit = true; + sGLThreadManager.notifyAll(); + while (! mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestReleaseEglContextLocked() { + mShouldReleaseEglContext = true; + sGLThreadManager.notifyAll(); + } + + /** + * Queue an "event" to be run on the GL rendering thread. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("r must not be null"); + } + synchronized(sGLThreadManager) { + mEventQueue.add(r); + sGLThreadManager.notifyAll(); + } + } + + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private boolean mShouldExit; + private boolean mExited; + private boolean mRequestPaused; + private boolean mPaused; + private boolean mHasSurface; + private boolean mSurfaceIsBad; + private boolean mWaitingForSurface; + private boolean mHaveEglContext; + private boolean mHaveEglSurface; + private boolean mFinishedCreatingEglSurface; + private boolean mShouldReleaseEglContext; + private int mWidth; + private int mHeight; + private int mRenderMode; + private boolean mRequestRender; + private boolean mWantRenderNotification; + private boolean mRenderComplete; + private ArrayList<Runnable> mEventQueue = new ArrayList<Runnable>(); + private boolean mSizeChanged = true; + private Runnable mFinishDrawingRunnable = null; + + // End of member variables protected by the sGLThreadManager monitor. + + private EglHelper mEglHelper; + + /** + * Set once at thread construction time, nulled out when the parent view is garbage + * called. This weak reference allows the GLSurfaceView to be garbage collected while + * the GLThread is still alive. + */ + private WeakReference<GLSurfaceView> mGLSurfaceViewWeakRef; + + } + + static class LogWriter extends Writer { + + @Override public void close() { + flushBuilder(); + } + + @Override public void flush() { + flushBuilder(); + } + + @Override public void write(char[] buf, int offset, int count) { + for(int i = 0; i < count; i++) { + char c = buf[offset + i]; + if ( c == '\n') { + flushBuilder(); + } + else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.v("GLSurfaceView", mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } + + private StringBuilder mBuilder = new StringBuilder(); + } + + + private void checkRenderThreadState() { + if (mGLThread != null) { + throw new IllegalStateException( + "setRenderer has already been called for this instance."); + } + } + + private static class GLThreadManager { + private static String TAG = "GLThreadManager"; + + public synchronized void threadExiting(GLThread thread) { + if (LOG_THREADS) { + Log.i("GLThread", "exiting tid=" + thread.getId()); + } + thread.mExited = true; + notifyAll(); + } + + /* + * Releases the EGL context. Requires that we are already in the + * sGLThreadManager monitor when this is called. + */ + public void releaseEglContextLocked(GLThread thread) { + notifyAll(); + } + } + + private static final GLThreadManager sGLThreadManager = new GLThreadManager(); + + private final WeakReference<GLSurfaceView> mThisWeakRef = + new WeakReference<GLSurfaceView>(this); + private GLThread mGLThread; + private Renderer mRenderer; + private boolean mDetached; + private EGLConfigChooser mEGLConfigChooser; + private EGLContextFactory mEGLContextFactory; + private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private GLWrapper mGLWrapper; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; +} + diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java index e3956ac459..5c4fd00f6d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java @@ -28,38 +28,38 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.godot; +package org.godotengine.godot.gl; +import org.godotengine.godot.GodotLib; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; -import org.godotengine.godot.utils.GLUtils; - -import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** - * Godot's renderer implementation. + * Godot's GL renderer implementation. */ -class GodotRenderer implements GLSurfaceView.Renderer { +public class GodotRenderer implements GLSurfaceView.Renderer { private final GodotPluginRegistry pluginRegistry; private boolean activityJustResumed = false; - GodotRenderer() { + public GodotRenderer() { this.pluginRegistry = GodotPluginRegistry.getPluginRegistry(); } - public void onDrawFrame(GL10 gl) { + public boolean onDrawFrame(GL10 gl) { if (activityJustResumed) { GodotLib.onRendererResumed(); activityJustResumed = false; } - GodotLib.step(); + boolean swapBuffers = GodotLib.step(); for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { plugin.onGLDrawFrame(gl); } + + return swapBuffers; } public void onSurfaceChanged(GL10 gl, int width, int height) { @@ -76,13 +76,13 @@ class GodotRenderer implements GLSurfaceView.Renderer { } } - void onActivityResumed() { + public void onActivityResumed() { // We defer invoking GodotLib.onRendererResumed() until the first draw frame call. // This ensures we have a valid GL context and surface when we do so. activityJustResumed = true; } - void onActivityPaused() { + public void onActivityPaused() { GodotLib.onRendererPaused(); } } 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 c06d89b843..8694bb91e1 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 @@ -34,8 +34,9 @@ import static org.godotengine.godot.utils.GLUtils.DEBUG; import org.godotengine.godot.GodotLib; import org.godotengine.godot.GodotRenderView; -import org.godotengine.godot.input.InputManagerCompat.InputDeviceListener; +import android.content.Context; +import android.hardware.input.InputManager; import android.os.Build; import android.util.Log; import android.util.SparseArray; @@ -53,9 +54,9 @@ import java.util.Set; /** * Handles input related events for the {@link GodotRenderView} view. */ -public class GodotInputHandler implements InputDeviceListener { +public class GodotInputHandler implements InputManager.InputDeviceListener { private final GodotRenderView mRenderView; - private final InputManagerCompat mInputManager; + private final InputManager mInputManager; private final String tag = this.getClass().getSimpleName(); @@ -64,7 +65,7 @@ public class GodotInputHandler implements InputDeviceListener { public GodotInputHandler(GodotRenderView godotView) { mRenderView = godotView; - mInputManager = InputManagerCompat.Factory.getInputManager(mRenderView.getView().getContext()); + mInputManager = (InputManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_SERVICE); mInputManager.registerInputDeviceListener(this, null); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java deleted file mode 100644 index 21fdc658bb..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.godotengine.godot.input; - -import android.content.Context; -import android.os.Handler; -import android.view.InputDevice; -import android.view.MotionEvent; - -public interface InputManagerCompat { - /** - * Gets information about the input device with the specified id. - * - * @param id The device id - * @return The input device or null if not found - */ - InputDevice getInputDevice(int id); - - /** - * Gets the ids of all input devices in the system. - * - * @return The input device ids. - */ - int[] getInputDeviceIds(); - - /** - * Registers an input device listener to receive notifications about when - * input devices are added, removed or changed. - * - * @param listener The listener to register. - * @param handler The handler on which the listener should be invoked, or - * null if the listener should be invoked on the calling thread's - * looper. - */ - void registerInputDeviceListener(InputManagerCompat.InputDeviceListener listener, - Handler handler); - - /** - * Unregisters an input device listener. - * - * @param listener The listener to unregister. - */ - void unregisterInputDeviceListener(InputManagerCompat.InputDeviceListener listener); - - /* - * The following three calls are to simulate V16 behavior on pre-Jellybean - * devices. If you don't call them, your callback will never be called - * pre-API 16. - */ - - /** - * Pass the motion events to the InputManagerCompat. This is used to - * optimize for polling for controllers. If you do not pass these events in, - * polling will cause regular object creation. - * - * @param event the motion event from the app - */ - void onGenericMotionEvent(MotionEvent event); - - /** - * Tell the V9 input manager that it should stop polling for disconnected - * devices. You can call this during onPause in your activity, although you - * might want to call it whenever your game is not active (or whenever you - * don't care about being notified of new input devices) - */ - void onPause(); - - /** - * Tell the V9 input manager that it should start polling for disconnected - * devices. You can call this during onResume in your activity, although you - * might want to call it less often (only when the gameplay is actually - * active) - */ - void onResume(); - - interface InputDeviceListener { - /** - * Called whenever the input manager detects that a device has been - * added. This will only be called in the V9 version when a motion event - * is detected. - * - * @param deviceId The id of the input device that was added. - */ - void onInputDeviceAdded(int deviceId); - - /** - * Called whenever the properties of an input device have changed since - * they were last queried. This will not be called for the V9 version of - * the API. - * - * @param deviceId The id of the input device that changed. - */ - void onInputDeviceChanged(int deviceId); - - /** - * Called whenever the input manager detects that a device has been - * removed. For the V9 version, this can take some time depending on the - * poll rate. - * - * @param deviceId The id of the input device that was removed. - */ - void onInputDeviceRemoved(int deviceId); - } - - /** - * Use this to construct a compatible InputManager. - */ - class Factory { - /** - * Constructs and returns a compatible InputManger - * - * @param context the Context that will be used to get the system - * service from - * @return a compatible implementation of InputManager - */ - public static InputManagerCompat getInputManager(Context context) { - return new InputManagerV16(context); - } - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java deleted file mode 100644 index 0dbc13c77b..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.godotengine.godot.input; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.input.InputManager; -import android.os.Build; -import android.os.Handler; -import android.view.InputDevice; -import android.view.MotionEvent; - -import java.util.HashMap; -import java.util.Map; - -@TargetApi(Build.VERSION_CODES.JELLY_BEAN) -public class InputManagerV16 implements InputManagerCompat { - private final InputManager mInputManager; - private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> mListeners; - - public InputManagerV16(Context context) { - mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); - mListeners = new HashMap<>(); - } - - @Override - public InputDevice getInputDevice(int id) { - return mInputManager.getInputDevice(id); - } - - @Override - public int[] getInputDeviceIds() { - return mInputManager.getInputDeviceIds(); - } - - static class V16InputDeviceListener implements InputManager.InputDeviceListener { - final InputManagerCompat.InputDeviceListener mIDL; - - public V16InputDeviceListener(InputDeviceListener idl) { - mIDL = idl; - } - - @Override - public void onInputDeviceAdded(int deviceId) { - mIDL.onInputDeviceAdded(deviceId); - } - - @Override - public void onInputDeviceChanged(int deviceId) { - mIDL.onInputDeviceChanged(deviceId); - } - - @Override - public void onInputDeviceRemoved(int deviceId) { - mIDL.onInputDeviceRemoved(deviceId); - } - } - - @Override - public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { - V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener); - mInputManager.registerInputDeviceListener(v16Listener, handler); - mListeners.put(listener, v16Listener); - } - - @Override - public void unregisterInputDeviceListener(InputDeviceListener listener) { - V16InputDeviceListener curListener = mListeners.remove(listener); - if (null != curListener) { - mInputManager.unregisterInputDeviceListener(curListener); - } - } - - @Override - public void onGenericMotionEvent(MotionEvent event) { - // unused in V16 - } - - @Override - public void onPause() { - // unused in V16 - } - - @Override - public void onResume() { - // unused in V16 - } -} 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 new file mode 100644 index 0000000000..2239ddac8e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java @@ -0,0 +1,298 @@ +/*************************************************************************/ +/* GodotTTS.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.tts; + +import org.godotengine.godot.GodotLib; + +import android.app.Activity; +import android.os.Bundle; +import android.speech.tts.TextToSpeech; +import android.speech.tts.UtteranceProgressListener; +import android.speech.tts.Voice; + +import androidx.annotation.Keep; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Set; + +/** + * Wrapper for Android Text to Speech API and custom utterance query implementation. + * <p> + * A [GodotTTS] provides the following features: + * <p> + * <ul> + * <li>Access to the Android Text to Speech API. + * <li>Utterance pause / resume functions, unsupported by Android TTS API. + * </ul> + */ +@Keep +public class GodotTTS extends UtteranceProgressListener { + // Note: These constants must be in sync with DisplayServer::TTSUtteranceEvent enum from "servers/display_server.h". + final private static int EVENT_START = 0; + final private static int EVENT_END = 1; + 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 Object lock = new Object(); + private GodotUtterance lastUtterance; + + private boolean speaking; + private boolean paused; + + public GodotTTS(Activity p_activity) { + synth = new TextToSpeech(p_activity, null); + queue = new LinkedList<GodotUtterance>(); + + synth.setOnUtteranceProgressListener(this); + } + + private void updateTTS() { + if (!speaking && queue.size() > 0) { + int mode = TextToSpeech.QUEUE_FLUSH; + GodotUtterance message = queue.pollFirst(); + + Set<Voice> voices = synth.getVoices(); + for (Voice v : voices) { + if (v.getName().equals(message.voice)) { + synth.setVoice(v); + break; + } + } + synth.setPitch(message.pitch); + synth.setSpeechRate(message.rate); + + Bundle params = new Bundle(); + params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, message.volume / 100.f); + + lastUtterance = message; + lastUtterance.start = 0; + lastUtterance.offset = 0; + paused = false; + + synth.speak(message.text, mode, params, String.valueOf(message.id)); + speaking = true; + } + } + + /** + * Called by TTS engine when the TTS service is about to speak the specified range. + */ + @Override + public void onRangeStart(String utteranceId, int start, int end, int frame) { + synchronized (lock) { + if (lastUtterance != null && Integer.parseInt(utteranceId) == lastUtterance.id) { + lastUtterance.offset = start; + GodotLib.ttsCallback(EVENT_BOUNDARY, lastUtterance.id, start + lastUtterance.start); + } + } + } + + /** + * Called by TTS engine when an utterance was canceled in progress. + */ + @Override + public void onStop(String utteranceId, boolean interrupted) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an utterance has begun to be spoken.. + */ + @Override + public void onStart(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && lastUtterance.start == 0 && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_START, lastUtterance.id, 0); + } + } + } + + /** + * Called by TTS engine when an utterance was successfully finished. + */ + @Override + public void onDone(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_END, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an error has occurred during processing. + */ + @Override + public void onError(String utteranceId, int errorCode) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an error has occurred during processing (pre API level 21 version). + */ + @Override + public void onError(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Adds an utterance to the queue. + */ + public void speak(String text, String voice, int volume, float pitch, float rate, int utterance_id, boolean interrupt) { + synchronized (lock) { + GodotUtterance message = new GodotUtterance(text, voice, volume, pitch, rate, utterance_id); + queue.addLast(message); + + if (isPaused()) { + resumeSpeaking(); + } else { + updateTTS(); + } + } + } + + /** + * Puts the synthesizer into a paused state. + */ + public void pauseSpeaking() { + synchronized (lock) { + if (!paused) { + paused = true; + synth.stop(); + } + } + } + + /** + * Resumes the synthesizer if it was paused. + */ + public void resumeSpeaking() { + synchronized (lock) { + if (lastUtterance != null && paused) { + int mode = TextToSpeech.QUEUE_FLUSH; + + Set<Voice> voices = synth.getVoices(); + for (Voice v : voices) { + if (v.getName().equals(lastUtterance.voice)) { + synth.setVoice(v); + break; + } + } + synth.setPitch(lastUtterance.pitch); + synth.setSpeechRate(lastUtterance.rate); + + Bundle params = new Bundle(); + params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, lastUtterance.volume / 100.f); + + lastUtterance.start = lastUtterance.offset; + lastUtterance.offset = 0; + paused = false; + + synth.speak(lastUtterance.text.substring(lastUtterance.start), mode, params, String.valueOf(lastUtterance.id)); + speaking = true; + } else { + paused = false; + } + } + } + + /** + * Stops synthesis in progress and removes all utterances from the queue. + */ + public void stopSpeaking() { + synchronized (lock) { + for (GodotUtterance u : queue) { + GodotLib.ttsCallback(EVENT_CANCEL, u.id, 0); + } + queue.clear(); + + if (lastUtterance != null) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + } + lastUtterance = null; + + paused = false; + speaking = false; + + synth.stop(); + } + } + + /** + * Returns voice information. + */ + public String[] getVoices() { + Set<Voice> voices = synth.getVoices(); + String[] list = new String[voices.size()]; + int i = 0; + for (Voice v : voices) { + list[i++] = v.getLocale().toString() + ";" + v.getName(); + } + return list; + } + + /** + * Returns true if the synthesizer is generating speech, or have utterance waiting in the queue. + */ + public boolean isSpeaking() { + return speaking; + } + + /** + * Returns true if the synthesizer is in a paused state. + */ + public boolean isPaused() { + return paused; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java new file mode 100644 index 0000000000..bde37e7315 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java @@ -0,0 +1,55 @@ +/*************************************************************************/ +/* GodotUtterance.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +package org.godotengine.godot.tts; + +/** + * A speech request for GodotTTS. + */ +class GodotUtterance { + final String text; + final String voice; + final int volume; + final float pitch; + final float rate; + final int id; + + int offset = -1; + int start = 0; + + GodotUtterance(String text, String voice, int volume, float pitch, float rate, int id) { + this.text = text; + this.voice = voice; + this.volume = volume; + this.pitch = pitch; + this.rate = rate; + this.id = id; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java index 39a57f587a..47df23fe1a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java @@ -43,8 +43,8 @@ public class Crypt { // Create Hex String StringBuilder hexString = new StringBuilder(); - for (int i = 0; i < messageDigest.length; i++) - hexString.append(Integer.toHexString(0xFF & messageDigest[i])); + for (byte b : messageDigest) + hexString.append(Integer.toHexString(0xFF & b)); return hexString.toString(); } catch (Exception e) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java index 4c1c84affb..e35d4f5828 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java @@ -30,8 +30,9 @@ package org.godotengine.godot.xr.ovr; +import org.godotengine.godot.gl.GLSurfaceView; + import android.opengl.EGLExt; -import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java index 2b4369b8a6..deb9c4bb1d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java @@ -30,8 +30,9 @@ package org.godotengine.godot.xr.ovr; +import org.godotengine.godot.gl.GLSurfaceView; + import android.opengl.EGL14; -import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java index fbfe0a3a75..f087b7dc74 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java @@ -30,7 +30,7 @@ package org.godotengine.godot.xr.ovr; -import android.opengl.GLSurfaceView; +import org.godotengine.godot.gl.GLSurfaceView; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java index 9fde1961ea..445238b1c2 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java @@ -30,10 +30,9 @@ package org.godotengine.godot.xr.regular; +import org.godotengine.godot.gl.GLSurfaceView; import org.godotengine.godot.utils.GLUtils; -import android.opengl.GLSurfaceView; - import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLDisplay; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java index ce1184a75c..5d62723170 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java @@ -30,9 +30,9 @@ package org.godotengine.godot.xr.regular; +import org.godotengine.godot.gl.GLSurfaceView; import org.godotengine.godot.utils.GLUtils; -import android.opengl.GLSurfaceView; import android.util.Log; import javax.microedition.khronos.egl.EGL10; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java index 420dda45a0..68329c5c49 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java @@ -30,8 +30,6 @@ package org.godotengine.godot.xr.regular; -import org.godotengine.godot.utils.GLUtils; - import android.util.Log; import javax.microedition.khronos.egl.EGL10; diff --git a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt index 966c02f7d7..711f7cd502 100644 --- a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt +++ b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt @@ -16,3 +16,5 @@ add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${GODOT_ROOT_DIR}) + +add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED) diff --git a/platform/android/java/nativeSrcsConfigs/build.gradle b/platform/android/java/nativeSrcsConfigs/build.gradle index 158bb2b98e..0cb769b539 100644 --- a/platform/android/java/nativeSrcsConfigs/build.gradle +++ b/platform/android/java/nativeSrcsConfigs/build.gradle @@ -6,6 +6,7 @@ plugins { android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion defaultConfig { minSdkVersion versions.minSdk @@ -28,8 +29,6 @@ android { } } - ndkVersion versions.ndkVersion - externalNativeBuild { cmake { path "CMakeLists.txt" diff --git a/platform/android/java/scripts/publish-module.gradle b/platform/android/java/scripts/publish-module.gradle index 6b2aea5caf..32b749e493 100644 --- a/platform/android/java/scripts/publish-module.gradle +++ b/platform/android/java/scripts/publish-module.gradle @@ -7,20 +7,15 @@ version = PUBLISH_VERSION afterEvaluate { publishing { publications { - release(MavenPublication) { + templateRelease(MavenPublication) { + from components.templateRelease + // The coordinates of the library, being set from variables that // we'll set up later groupId ossrhGroupId artifactId PUBLISH_ARTIFACT_ID version PUBLISH_VERSION - // Two artifacts, the `aar` (or `jar`) and the sources - if (project.plugins.findPlugin("com.android.library")) { - from components.release - } else { - from components.java - } - // Mostly self-explanatory metadata pom { name = PUBLISH_ARTIFACT_ID diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index 584b626900..56e1b6fd3a 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -4,6 +4,7 @@ rootProject.name = "Godot" include ':app' include ':lib' include ':nativeSrcsConfigs' +include ':editor' include ':assetPacks:installTime' project(':assetPacks:installTime').projectDir = file("app/assetPacks/installTime") diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index d6e3ad90b1..5b21e696c3 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -54,6 +54,7 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc _get_locale = p_env->GetMethodID(cls, "getLocale", "()Ljava/lang/String;"); _get_model = p_env->GetMethodID(cls, "getModel", "()Ljava/lang/String;"); _get_screen_DPI = p_env->GetMethodID(cls, "getScreenDPI", "()I"); + _get_scaled_density = p_env->GetMethodID(cls, "getScaledDensity", "()F"); _get_screen_refresh_rate = p_env->GetMethodID(cls, "getScreenRefreshRate", "(D)D"); _screen_get_usable_rect = p_env->GetMethodID(cls, "screenGetUsableRect", "()[I"), _get_unique_id = p_env->GetMethodID(cls, "getUniqueID", "()Ljava/lang/String;"); @@ -138,6 +139,16 @@ int GodotIOJavaWrapper::get_screen_dpi() { } } +float GodotIOJavaWrapper::get_scaled_density() { + if (_get_scaled_density) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 1.0f); + return env->CallFloatMethod(godot_io_instance, _get_scaled_density); + } else { + return 1.0f; + } +} + float GodotIOJavaWrapper::get_screen_refresh_rate(float fallback) { if (_get_screen_refresh_rate) { JNIEnv *env = get_jni_env(); diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index 38a2b710a9..08e3092afd 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -51,6 +51,7 @@ private: jmethodID _get_locale = 0; jmethodID _get_model = 0; jmethodID _get_screen_DPI = 0; + jmethodID _get_scaled_density = 0; jmethodID _get_screen_refresh_rate = 0; jmethodID _screen_get_usable_rect = 0; jmethodID _get_unique_id = 0; @@ -72,6 +73,7 @@ public: String get_locale(); String get_model(); int get_screen_dpi(); + float get_scaled_density(); float get_screen_refresh_rate(float fallback); void screen_get_usable_rect(int (&p_rect_xywh)[4]); String get_unique_id(); diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 249717921f..8ad72b499e 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -49,6 +49,7 @@ #include "os_android.h" #include "string_android.h" #include "thread_jandroid.h" +#include "tts_android.h" #include <android/input.h> #include <unistd.h> @@ -96,6 +97,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *en DirAccessJAndroid::setup(godot_io_java->get_instance()); NetSocketAndroid::setup(godot_java->get_member_object("netUtils", "Lorg/godotengine/godot/utils/GodotNetUtils;", env)); + TTS_Android::setup(godot_java->get_member_object("tts", "Lorg/godotengine/godot/tts/GodotTTS;", env)); os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion); @@ -107,6 +109,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *en JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz) { // lets cleanup + if (java_class_wrapper) { + memdelete(java_class_wrapper); + } if (godot_io_java) { delete godot_io_java; } @@ -117,6 +122,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env delete input_handler; } if (os_android) { + os_android->main_loop_end(); delete os_android; } } @@ -146,7 +152,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc } } - Error err = Main::setup("apk", cmdlen, (char **)cmdline, false); + Error err = Main::setup(OS_Android::ANDROID_EXEC_PATH, cmdlen, (char **)cmdline, false); if (cmdline) { if (j_cmdline) { for (int i = 0; i < cmdlen; ++i) { @@ -209,9 +215,13 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jcl } } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos) { + TTS_Android::_java_utterance_callback(event, id, pos); +} + +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz) { if (step.get() == -1) { - return; + return true; } if (step.get() == 0) { @@ -220,12 +230,12 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jcl Main::setup2(Thread::get_caller_id()); input_handler = new AndroidInputHandler(); step.increment(); - return; + return true; } if (step.get() == 1) { if (!Main::start()) { - return; // should exit instead and print the error + return true; // should exit instead and print the error } godot_java->on_godot_setup_completed(env); @@ -239,9 +249,12 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jcl DisplayServerAndroid::get_singleton()->process_magnetometer(magnetometer); DisplayServerAndroid::get_singleton()->process_gyroscope(gyroscope); - if (os_android->main_loop_iterate()) { + bool should_swap_buffers = false; + if (os_android->main_loop_iterate(&should_swap_buffers)) { godot_java->force_quit(env); } + + return should_swap_buffers; } void touch_preprocessing(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask, jfloat vertical_factor, jfloat horizontal_factor) { @@ -496,6 +509,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI return; } + // We force redraw to ensure we render at least once when resuming the app. + Main::force_redraw(); if (os_android->get_main_loop()) { os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_RESUMED); } diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 927b44ddb6..4f2195942c 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -42,7 +42,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline); 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 void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz); void touch_preprocessing(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask = 0, jfloat vertical_factor = 0, jfloat horizontal_factor = 0); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3F(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions); diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 754267c834..2c8378e685 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -77,6 +77,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;"); _on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V"); _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); + _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)V"); // get some Activity method pointers... _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); @@ -351,3 +352,16 @@ void GodotJavaWrapper::vibrate(int p_duration_ms) { env->CallVoidMethod(godot_instance, _vibrate, p_duration_ms); } } + +void GodotJavaWrapper::create_new_godot_instance(List<String> args) { + if (_create_new_godot_instance) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + + jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")); + for (int i = 0; i < args.size(); i++) { + env->SetObjectArrayElement(jargs, i, env->NewStringUTF(args[i].utf8().get_data())); + } + env->CallVoidMethod(godot_instance, _create_new_godot_instance, jargs); + } +} diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 42ae91480f..f04fda7c3d 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -37,6 +37,7 @@ #include <android/log.h> #include <jni.h> +#include "core/templates/list.h" #include "java_godot_view_wrapper.h" #include "string_android.h" @@ -70,6 +71,7 @@ private: jmethodID _on_godot_setup_completed = 0; jmethodID _on_godot_main_loop_started = 0; jmethodID _get_class_loader = 0; + jmethodID _create_new_godot_instance = 0; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); @@ -103,6 +105,7 @@ public: bool is_activity_resumed(); void vibrate(int p_duration_ms); String get_input_fallback_mapping(); + void create_new_godot_instance(List<String> args); }; #endif /* !JAVA_GODOT_WRAPPER_H */ diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index b17b0f3139..fc46005b41 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -35,6 +35,8 @@ #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" @@ -45,6 +47,8 @@ #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) { // Workaround for Android 6.0+ using a symlink. // Save the current directory. @@ -81,18 +85,28 @@ void OS_Android::alert(const String &p_alert, const String &p_title) { void OS_Android::initialize_core() { OS_Unix::initialize_core(); +#ifdef TOOLS_ENABLED + FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES); +#else if (use_apk_expansion) { FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES); } else { FileAccess::make_default<FileAccessAndroid>(FileAccess::ACCESS_RESOURCES); } +#endif FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_USERDATA); FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_FILESYSTEM); + +#ifdef TOOLS_ENABLED + DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_RESOURCES); +#else if (use_apk_expansion) { DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_RESOURCES); } else { DirAccess::make_default<DirAccessJAndroid>(DirAccess::ACCESS_RESOURCES); } +#endif + DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_USERDATA); DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_FILESYSTEM); @@ -125,7 +139,7 @@ void OS_Android::finalize() { } OS_Android *OS_Android::get_singleton() { - return (OS_Android *)OS::get_singleton(); + return static_cast<OS_Android *>(OS::get_singleton()); } GodotJavaWrapper *OS_Android::get_godot_java() { @@ -168,16 +182,27 @@ void OS_Android::main_loop_begin() { } } -bool OS_Android::main_loop_iterate() { +bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) { if (!main_loop) { return false; } DisplayServerAndroid::get_singleton()->process_events(); - return Main::iteration(); + uint64_t current_frames_drawn = Engine::get_singleton()->get_frames_drawn(); + bool exit = Main::iteration(); + + if (r_should_swap_buffers) { + *r_should_swap_buffers = !is_in_low_processor_usage_mode() || RenderingServer::get_singleton()->has_changed() || current_frames_drawn != Engine::get_singleton()->get_frames_drawn(); + } + + return exit; } void OS_Android::main_loop_end() { if (main_loop) { + SceneTree *scene_tree = Object::cast_to<SceneTree>(main_loop); + if (scene_tree) { + scene_tree->quit(); + } main_loop->finalize(); } } @@ -197,7 +222,11 @@ Error OS_Android::shell_open(String p_uri) { } 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 +#endif } String OS_Android::get_locale() const { @@ -222,6 +251,14 @@ String OS_Android::get_data_path() const { return get_user_data_dir(); } +String OS_Android::get_executable_path() const { + // Since unix process creation is restricted on Android, we bypass + // OS_Unix::get_executable_path() so we can return ANDROID_EXEC_PATH. + // Detection of ANDROID_EXEC_PATH allows to handle process creation in an Android compliant + // manner. + return OS::get_executable_path(); +} + String OS_Android::get_user_data_dir() const { if (!data_dir_cache.is_empty()) { return data_dir_cache; @@ -294,6 +331,10 @@ void OS_Android::vibrate_handheld(int p_duration_ms) { godot_java->vibrate(p_duration_ms); } +String OS_Android::get_config_path() const { + return get_user_data_dir().plus_file("config"); +} + bool OS_Android::_check_internal_feature_support(const String &p_feature) { if (p_feature == "mobile") { return true; @@ -343,5 +384,26 @@ OS_Android::OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_god DisplayServerAndroid::register_android_driver(); } +Error OS_Android::execute(const String &p_path, const List<String> &p_arguments, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex, bool p_open_console) { + if (p_path == ANDROID_EXEC_PATH) { + return create_instance(p_arguments); + } else { + return OS_Unix::execute(p_path, p_arguments, r_pipe, r_exitcode, read_stderr, p_pipe_mutex, p_open_console); + } +} + +Error OS_Android::create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id, bool p_open_console) { + if (p_path == ANDROID_EXEC_PATH) { + return create_instance(p_arguments, r_child_id); + } else { + return OS_Unix::create_process(p_path, p_arguments, r_child_id, p_open_console); + } +} + +Error OS_Android::create_instance(const List<String> &p_arguments, ProcessID *r_child_id) { + godot_java->create_new_godot_instance(p_arguments); + return OK; +} + OS_Android::~OS_Android() { } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index f523f172c6..a40e17dc2c 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -52,7 +52,7 @@ private: #endif #if defined(VULKAN_ENABLED) - ANativeWindow *native_window; + ANativeWindow *native_window = nullptr; #endif mutable String data_dir_cache; @@ -60,12 +60,14 @@ private: AudioDriverOpenSL audio_driver_android; - MainLoop *main_loop; + MainLoop *main_loop = nullptr; - GodotJavaWrapper *godot_java; - GodotIOJavaWrapper *godot_io_java; + GodotJavaWrapper *godot_java = nullptr; + GodotIOJavaWrapper *godot_io_java = nullptr; public: + static const char *ANDROID_EXEC_PATH; + virtual void initialize_core() override; virtual void initialize() override; @@ -94,7 +96,7 @@ public: virtual MainLoop *get_main_loop() const override; void main_loop_begin(); - bool main_loop_iterate(); + bool main_loop_iterate(bool *r_should_swap_buffers = nullptr); void main_loop_end(); void main_loop_focusout(); void main_loop_focusin(); @@ -108,6 +110,7 @@ public: ANativeWindow *get_native_window() const; virtual Error shell_open(String p_uri) override; + virtual String get_executable_path() const override; virtual String get_user_data_dir() const override; virtual String get_data_path() const override; virtual String get_cache_path() const override; @@ -121,6 +124,12 @@ public: void vibrate_handheld(int p_duration_ms) override; + virtual String get_config_path() const override; + + virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override; + 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 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); ~OS_Android(); diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp new file mode 100644 index 0000000000..528878f14e --- /dev/null +++ b/platform/android/tts_android.cpp @@ -0,0 +1,189 @@ +/*************************************************************************/ +/* tts_android.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#include "tts_android.h" + +#include "java_godot_wrapper.h" +#include "os_android.h" +#include "string_android.h" +#include "thread_jandroid.h" + +jobject TTS_Android::tts = 0; +jclass TTS_Android::cls = 0; + +jmethodID TTS_Android::_is_speaking = 0; +jmethodID TTS_Android::_is_paused = 0; +jmethodID TTS_Android::_get_voices = 0; +jmethodID TTS_Android::_speak = 0; +jmethodID TTS_Android::_pause_speaking = 0; +jmethodID TTS_Android::_resume_speaking = 0; +jmethodID TTS_Android::_stop_speaking = 0; + +Map<int, Char16String> TTS_Android::ids; + +void TTS_Android::setup(jobject p_tts) { + JNIEnv *env = get_jni_env(); + + tts = env->NewGlobalRef(p_tts); + + jclass c = env->GetObjectClass(tts); + cls = (jclass)env->NewGlobalRef(c); + + _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"); +} + +void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) { + if (ids.has(p_id)) { + int pos = 0; + if ((DisplayServer::TTSUtteranceEvent)p_event == DisplayServer::TTS_UTTERANCE_BOUNDARY) { + // Convert position from UTF-16 to UTF-32. + const Char16String &string = ids[p_id]; + for (int i = 0; i < MIN(p_pos, string.length()); i++) { + char16_t c = string[i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + } else if ((DisplayServer::TTSUtteranceEvent)p_event != DisplayServer::TTS_UTTERANCE_STARTED) { + ids.erase(p_id); + } + DisplayServer::get_singleton()->tts_post_utterance_event((DisplayServer::TTSUtteranceEvent)p_event, p_id, pos); + } +} + +bool TTS_Android::is_speaking() { + if (_is_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(tts, _is_speaking); + } else { + return false; + } +} + +bool TTS_Android::is_paused() { + if (_is_paused) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(tts, _is_paused); + } else { + return false; + } +} + +Array TTS_Android::get_voices() { + Array list; + if (_get_voices) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, list); + + jobject voices_object = env->CallObjectMethod(tts, _get_voices); + jobjectArray *arr = reinterpret_cast<jobjectArray *>(&voices_object); + + jsize len = env->GetArrayLength(*arr); + for (int i = 0; i < len; i++) { + jstring jStr = (jstring)env->GetObjectArrayElement(*arr, i); + String str = jstring_to_string(jStr, env); + Vector<String> tokens = str.split(";", true, 2); + if (tokens.size() == 2) { + Dictionary voice_d; + voice_d["name"] = tokens[1]; + voice_d["id"] = tokens[1]; + voice_d["language"] = tokens[0]; + list.push_back(voice_d); + } + env->DeleteLocalRef(jStr); + } + } + return list; +} + +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) { + if (p_interrupt) { + stop(); + } + + if (p_text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, p_utterance_id); + return; + } + + ids[p_utterance_id] = p_text.utf16(); + + if (_speak) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + + jstring jStrT = env->NewStringUTF(p_text.utf8().get_data()); + jstring jStrV = env->NewStringUTF(p_voice.utf8().get_data()); + env->CallVoidMethod(tts, _speak, jStrT, jStrV, CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, p_interrupt); + } +} + +void TTS_Android::pause() { + if (_pause_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _pause_speaking); + } +} + +void TTS_Android::resume() { + if (_resume_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _resume_speaking); + } +} + +void TTS_Android::stop() { + for (Map<int, Char16String>::Element *E = ids.front(); E; E = E->next()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, E->key()); + } + ids.clear(); + + if (_stop_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _stop_speaking); + } +} diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h new file mode 100644 index 0000000000..efeed94856 --- /dev/null +++ b/platform/android/tts_android.h @@ -0,0 +1,67 @@ +/*************************************************************************/ +/* tts_android.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#ifndef TTS_ANDROID_H +#define TTS_ANDROID_H + +#include "core/string/ustring.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +#include <jni.h> + +class TTS_Android { + static jobject tts; + static jclass cls; + + static jmethodID _is_speaking; + static jmethodID _is_paused; + static jmethodID _get_voices; + static jmethodID _speak; + static jmethodID _pause_speaking; + static jmethodID _resume_speaking; + static jmethodID _stop_speaking; + + static Map<int, Char16String> ids; + +public: + static void setup(jobject p_tts); + static void _java_utterance_callback(int p_event, int p_id, int p_pos); + + static bool is_speaking(); + static bool is_paused(); + static Array get_voices(); + static void 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); + static void pause(); + static void resume(); + static void stop(); +}; + +#endif // TTS_ANDROID_H |