diff options
Diffstat (limited to 'platform/android')
97 files changed, 7455 insertions, 1444 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index d031d14499..d370a4d18d 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -6,8 +6,10 @@ android_files = [ "os_android.cpp", "android_input_handler.cpp", "file_access_android.cpp", + "file_access_filesystem_jandroid.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 +55,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/android_input_handler.cpp b/platform/android/android_input_handler.cpp index 10f23b320b..81802298d9 100644 --- a/platform/android/android_input_handler.cpp +++ b/platform/android/android_input_handler.cpp @@ -381,13 +381,3 @@ MouseButton AndroidInputHandler::_android_button_mask_to_godot_button_mask(int a return godot_button_mask; } - -void AndroidInputHandler::process_scroll(Point2 p_pos) { - Ref<InputEventPanGesture> ev; - ev.instantiate(); - _set_key_modifier_state(ev); - ev->set_position(p_pos); - ev->set_delta(p_pos - scroll_prev_pos); - Input::get_singleton()->parse_input_event(ev); - scroll_prev_pos = p_pos; -} diff --git a/platform/android/android_input_handler.h b/platform/android/android_input_handler.h index 2b237c4006..1397ca59e4 100644 --- a/platform/android/android_input_handler.h +++ b/platform/android/android_input_handler.h @@ -69,7 +69,6 @@ private: Vector<TouchPos> touch; Point2 hover_prev_pos; // needed to calculate the relative position on hover events - Point2 scroll_prev_pos; // needed to calculate the relative position on scroll events void _set_key_modifier_state(Ref<InputEventWithModifiers> ev); @@ -83,9 +82,8 @@ public: void process_hover(int p_type, Point2 p_pos); void process_mouse_event(int input_device, int event_action, int event_android_buttons_mask, Point2 event_pos, float event_vertical_factor = 0, float event_horizontal_factor = 0); void process_double_tap(int event_android_button_mask, Point2 p_pos); - void process_scroll(Point2 p_pos); void process_joy_event(JoypadEvent p_event); void process_key_event(int p_keycode, int p_scancode, int p_unicode_char, bool p_pressed); }; -#endif +#endif // ANDROID_INPUT_HANDLER_H diff --git a/platform/android/api/api.cpp b/platform/android/api/api.cpp index f544f29b10..f80f1e3051 100644 --- a/platform/android/api/api.cpp +++ b/platform/android/api/api.cpp @@ -64,14 +64,14 @@ void JavaClassWrapper::_bind_methods() { #if !defined(ANDROID_ENABLED) -Variant JavaClass::call(const StringName &, const Variant **, int, Callable::CallError &) { +Variant JavaClass::callp(const StringName &, const Variant **, int, Callable::CallError &) { return Variant(); } JavaClass::JavaClass() { } -Variant JavaObject::call(const StringName &, const Variant **, int, Callable::CallError &) { +Variant JavaObject::callp(const StringName &, const Variant **, int, Callable::CallError &) { return Variant(); } diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h index d4d1208757..ac8d6585d3 100644 --- a/platform/android/api/java_class_wrapper.h +++ b/platform/android/api/java_class_wrapper.h @@ -63,7 +63,7 @@ class JavaClass : public RefCounted { ARG_TYPE_MASK = (1 << 16) - 1 }; - Map<StringName, Variant> constant_map; + RBMap<StringName, Variant> constant_map; struct MethodInfo { bool _static = false; @@ -174,12 +174,12 @@ class JavaClass : public RefCounted { bool _call_method(JavaObject *p_instance, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error, Variant &ret); friend class JavaClassWrapper; - Map<StringName, List<MethodInfo>> methods; + HashMap<StringName, List<MethodInfo>> methods; jclass _class; #endif public: - virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; + virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; JavaClass(); }; @@ -195,7 +195,7 @@ class JavaObject : public RefCounted { #endif public: - virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; + virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; #ifdef ANDROID_ENABLED JavaObject(const Ref<JavaClass> &p_base, jobject *p_instance); @@ -207,7 +207,7 @@ class JavaClassWrapper : public Object { GDCLASS(JavaClassWrapper, Object); #ifdef ANDROID_ENABLED - Map<String, Ref<JavaClass>> class_cache; + RBMap<String, Ref<JavaClass>> class_cache; friend class JavaClass; jclass activityClass; jmethodID findClass; diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h index 57d08ac83e..690fddae21 100644 --- a/platform/android/api/jni_singleton.h +++ b/platform/android/api/jni_singleton.h @@ -48,13 +48,13 @@ class JNISingleton : public Object { }; jobject instance; - Map<StringName, MethodData> method_map; + RBMap<StringName, MethodData> method_map; #endif public: - virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override { + virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override { #ifdef ANDROID_ENABLED - Map<StringName, MethodData>::Element *E = method_map.find(p_method); + RBMap<StringName, MethodData>::Element *E = method_map.find(p_method); // Check the method we're looking for is in the JNISingleton map and that // the arguments match. @@ -70,10 +70,10 @@ public: if (call_error) { // The method is not in this map, defaulting to the regular instance calls. - return Object::call(p_method, p_args, p_argcount, r_error); + return Object::callp(p_method, p_args, p_argcount, r_error); } - ERR_FAIL_COND_V(!instance, Variant()); + ERR_FAIL_NULL_V(instance, Variant()); r_error.error = Callable::CallError::CALL_OK; @@ -176,7 +176,7 @@ public: #else // ANDROID_ENABLED // Defaulting to the regular instance calls. - return Object::call(p_method, p_args, p_argcount, r_error); + return Object::callp(p_method, p_args, p_argcount, r_error); #endif } diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 8495d2cc18..6b22a0ffa1 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -75,13 +75,11 @@ 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); } -AudioDriverOpenSL *AudioDriverOpenSL::s_ad = nullptr; - const char *AudioDriverOpenSL::get_name() const { return "Android"; } @@ -133,8 +131,6 @@ void AudioDriverOpenSL::start() { ERR_FAIL_COND(res != SL_RESULT_SUCCESS); SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, BUFFER_COUNT }; - //bufferQueue.locatorType = SL_DATALOCATOR_BUFFERQUEUE; - //bufferQueue.numBuffers = BUFFER_COUNT; /* Four buffers in our buffer queue */ /* Setup the format of the content in the buffer queue */ pcm.formatType = SL_DATAFORMAT_PCM; pcm.numChannels = 2; @@ -155,13 +151,8 @@ void AudioDriverOpenSL::start() { locator_outputmix.outputMix = OutputMix; audioSink.pLocator = (void *)&locator_outputmix; audioSink.pFormat = nullptr; - /* Initialize the context for Buffer queue callbacks */ - //cntxt.pDataBase = (void*)&pcmData; - //cntxt.pData = cntxt.pDataBase; - //cntxt.size = sizeof(pcmData); /* Create the music player */ - { const SLInterfaceID ids[2] = { SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND }; const SLboolean req[2] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE }; @@ -208,7 +199,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); } @@ -341,5 +332,4 @@ void AudioDriverOpenSL::set_pause(bool p_pause) { } AudioDriverOpenSL::AudioDriverOpenSL() { - s_ad = this; } diff --git a/platform/android/audio_driver_opensl.h b/platform/android/audio_driver_opensl.h index eea1fc227f..7b09438858 100644 --- a/platform/android/audio_driver_opensl.h +++ b/platform/android/audio_driver_opensl.h @@ -105,4 +105,4 @@ public: AudioDriverOpenSL(); }; -#endif // AUDIO_DRIVER_ANDROID_H +#endif // AUDIO_DRIVER_OPENSL_H diff --git a/platform/android/detect.py b/platform/android/detect.py index 3319d5890c..2ff5bf59ea 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -1,7 +1,7 @@ import os import sys import platform -from distutils.version import LooseVersion +import subprocess def is_active(): @@ -13,41 +13,35 @@ def get_name(): def can_build(): - return ("ANDROID_SDK_ROOT" in os.environ) or ("ANDROID_HOME" in os.environ) - - -def get_platform(platform): - return int(platform.split("-")[1]) + return os.path.exists(get_env_android_sdk_root()) def get_opts(): from SCons.Variables import BoolVariable, EnumVariable return [ - ("ANDROID_NDK_ROOT", "Path to the Android NDK", get_android_ndk_root()), - ("ANDROID_SDK_ROOT", "Path to the Android SDK", get_android_sdk_root()), + ("ANDROID_SDK_ROOT", "Path to the Android SDK", get_env_android_sdk_root()), ("ndk_platform", 'Target platform (android-<api>, e.g. "android-24")', "android-24"), EnumVariable("android_arch", "Target architecture", "arm64v8", ("armv7", "arm64v8", "x86", "x86_64")), ] # Return the ANDROID_SDK_ROOT environment variable. -# While ANDROID_HOME has been deprecated, it's used as a fallback for backward -# compatibility purposes. -def get_android_sdk_root(): - if "ANDROID_SDK_ROOT" in os.environ: - return os.environ.get("ANDROID_SDK_ROOT", 0) - else: - return os.environ.get("ANDROID_HOME", 0) +def get_env_android_sdk_root(): + return os.environ.get("ANDROID_SDK_ROOT", -1) -# Return the ANDROID_NDK_ROOT environment variable. -# We generate one for this build using the ANDROID_SDK_ROOT env -# variable and the project ndk version. -# If the env variable is already defined, we override it with -# our own to match what the project expects. -def get_android_ndk_root(): - return get_android_sdk_root() + "/ndk/" + get_project_ndk_version() +def get_min_sdk_version(platform): + return int(platform.split("-")[1]) + + +def get_android_ndk_root(env): + return env["ANDROID_SDK_ROOT"] + "/ndk/" + get_ndk_version() + + +# This is kept in sync with the value in 'platform/android/java/app/config.gradle'. +def get_ndk_version(): + return "23.2.8568313" def get_flags(): @@ -56,147 +50,80 @@ def get_flags(): ] -def create(env): - tools = env["TOOLS"] - if "mingw" in tools: - tools.remove("mingw") - if "applelink" in tools: - tools.remove("applelink") - env.Tool("gcc") - return env.Clone(tools=tools) - - -# Check if ANDROID_NDK_ROOT is valid. -# If not, install the ndk using ANDROID_SDK_ROOT and sdkmanager. +# Check if Android NDK version is installed +# If not, install it. def install_ndk_if_needed(env): print("Checking for Android NDK...") - env_ndk_version = get_env_ndk_version(env["ANDROID_NDK_ROOT"]) - if env_ndk_version is None: - # Reinstall the ndk and update ANDROID_NDK_ROOT. - print("Installing Android NDK...") - if env["ANDROID_SDK_ROOT"] is None: - raise Exception("Invalid ANDROID_SDK_ROOT environment variable.") - - import subprocess - + sdk_root = env["ANDROID_SDK_ROOT"] + if not os.path.exists(get_android_ndk_root(env)): extension = ".bat" if os.name == "nt" else "" - sdkmanager_path = env["ANDROID_SDK_ROOT"] + "/cmdline-tools/latest/bin/sdkmanager" + extension - ndk_download_args = "ndk;" + get_project_ndk_version() - subprocess.check_call([sdkmanager_path, ndk_download_args]) - - env["ANDROID_NDK_ROOT"] = env["ANDROID_SDK_ROOT"] + "/ndk/" + get_project_ndk_version() - print("ANDROID_NDK_ROOT: " + env["ANDROID_NDK_ROOT"]) + sdkmanager = sdk_root + "/cmdline-tools/latest/bin/sdkmanager" + extension + if os.path.exists(sdkmanager): + # Install the Android NDK + print("Installing Android NDK...") + ndk_download_args = "ndk;" + get_ndk_version() + subprocess.check_call([sdkmanager, ndk_download_args]) + else: + print("Cannot find " + sdkmanager) + print( + "Please ensure ANDROID_SDK_ROOT is correct and cmdline-tools are installed, or install NDK version " + + get_ndk_version() + + " manually." + ) + sys.exit() + env["ANDROID_NDK_ROOT"] = get_android_ndk_root(env) def configure(env): install_ndk_if_needed(env) - - # Workaround for MinGW. See: - # https://www.scons.org/wiki/LongCmdLinesOnWin32 - if os.name == "nt": - - import subprocess - - def mySubProcess(cmdline, env): - # print("SPAWNED : " + cmdline) - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - proc = subprocess.Popen( - cmdline, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - startupinfo=startupinfo, - shell=False, - env=env, - ) - data, err = proc.communicate() - rv = proc.wait() - if rv: - print("=====") - print(err) - print("=====") - return rv - - def mySpawn(sh, escape, cmd, args, env): - - newargs = " ".join(args[1:]) - cmdline = cmd + " " + newargs - - rv = 0 - if len(cmdline) > 32000 and cmd.endswith("ar"): - cmdline = cmd + " " + args[1] + " " + args[2] + " " - for i in range(3, len(args)): - rv = mySubProcess(cmdline + args[i], env) - if rv: - break - else: - rv = mySubProcess(cmdline, env) - - return rv - - env["SPAWN"] = mySpawn + ndk_root = env["ANDROID_NDK_ROOT"] # Architecture if env["android_arch"] not in ["armv7", "arm64v8", "x86", "x86_64"]: - env["android_arch"] = "armv7" + env["android_arch"] = "arm64v8" print("Building for Android, platform " + env["ndk_platform"] + " (" + env["android_arch"] + ")") - can_vectorize = True - if env["android_arch"] == "x86": - env["ARCH"] = "arch-x86" - env.extra_suffix = ".x86" + env.extra_suffix - target_subpath = "x86-4.9" - abi_subpath = "i686-linux-android" - arch_subpath = "x86" - env["x86_libtheora_opt_gcc"] = True - elif env["android_arch"] == "x86_64": - if get_platform(env["ndk_platform"]) < 21: + if get_min_sdk_version(env["ndk_platform"]) < 21: + if env["android_arch"] == "x86_64" or env["android_arch"] == "arm64v8": print( - "WARNING: android_arch=x86_64 is not supported by ndk_platform lower than android-21; setting" - " ndk_platform=android-21" + "WARNING: android_arch=" + + env["android_arch"] + + " is not supported by ndk_platform lower than android-21; setting ndk_platform=android-21" ) env["ndk_platform"] = "android-21" - env["ARCH"] = "arch-x86_64" - env.extra_suffix = ".x86_64" + env.extra_suffix - target_subpath = "x86_64-4.9" - abi_subpath = "x86_64-linux-android" - arch_subpath = "x86_64" - env["x86_libtheora_opt_gcc"] = True - elif env["android_arch"] == "armv7": - env["ARCH"] = "arch-arm" - target_subpath = "arm-linux-androideabi-4.9" - abi_subpath = "arm-linux-androideabi" - arch_subpath = "armeabi-v7a" + + if env["android_arch"] == "armv7": + target_triple = "armv7a-linux-androideabi" env.extra_suffix = ".armv7" + env.extra_suffix elif env["android_arch"] == "arm64v8": - if get_platform(env["ndk_platform"]) < 21: - print( - "WARNING: android_arch=arm64v8 is not supported by ndk_platform lower than android-21; setting" - " ndk_platform=android-21" - ) - env["ndk_platform"] = "android-21" - env["ARCH"] = "arch-arm64" - target_subpath = "aarch64-linux-android-4.9" - abi_subpath = "aarch64-linux-android" - arch_subpath = "arm64-v8a" + target_triple = "aarch64-linux-android" env.extra_suffix = ".armv8" + env.extra_suffix + elif env["android_arch"] == "x86": + target_triple = "i686-linux-android" + env.extra_suffix = ".x86" + env.extra_suffix + elif env["android_arch"] == "x86_64": + target_triple = "x86_64-linux-android" + env.extra_suffix = ".x86_64" + env.extra_suffix + + target_option = ["-target", target_triple + str(get_min_sdk_version(env["ndk_platform"]))] + env.Append(ASFLAGS=[target_option, "-c"]) + env.Append(CCFLAGS=target_option) + env.Append(LINKFLAGS=target_option) # Build type if env["target"].startswith("release"): if env["optimize"] == "speed": # optimize for speed (default) - env.Append(LINKFLAGS=["-O2"]) - env.Append(CCFLAGS=["-O2", "-fomit-frame-pointer"]) + # `-O2` is more friendly to debuggers than `-O3`, leading to better crash backtraces + # when using `target=release_debug`. + opt = "-O3" if env["target"] == "release" else "-O2" + env.Append(CCFLAGS=[opt, "-fomit-frame-pointer"]) elif env["optimize"] == "size": # optimize for size - env.Append(CCFLAGS=["-Os"]) - env.Append(LINKFLAGS=["-Os"]) - + env.Append(CCFLAGS=["-Oz"]) env.Append(CPPDEFINES=["NDEBUG"]) - if can_vectorize: - env.Append(CCFLAGS=["-ftree-vectorize"]) + env.Append(CCFLAGS=["-ftree-vectorize"]) elif env["target"] == "debug": env.Append(LINKFLAGS=["-O0"]) env.Append(CCFLAGS=["-O0", "-g", "-fno-limit-debug-info"]) @@ -208,7 +135,6 @@ def configure(env): env["SHLIBSUFFIX"] = ".so" if env["PLATFORM"] == "win32": - env.Tool("gcc") env.use_windows_spawn_fix() if sys.platform.startswith("linux"): @@ -221,32 +147,14 @@ def configure(env): else: host_subpath = "windows" - compiler_path = env["ANDROID_NDK_ROOT"] + "/toolchains/llvm/prebuilt/" + host_subpath + "/bin" - gcc_toolchain_path = env["ANDROID_NDK_ROOT"] + "/toolchains/" + target_subpath + "/prebuilt/" + host_subpath - tools_path = gcc_toolchain_path + "/" + abi_subpath + "/bin" - - # For Clang to find NDK tools in preference of those system-wide - env.PrependENVPath("PATH", tools_path) - - ccache_path = os.environ.get("CCACHE") - if ccache_path is None: - env["CC"] = compiler_path + "/clang" - env["CXX"] = compiler_path + "/clang++" - else: - # there aren't any ccache wrappers available for Android, - # to enable caching we need to prepend the path to the ccache binary - env["CC"] = ccache_path + " " + compiler_path + "/clang" - env["CXX"] = ccache_path + " " + compiler_path + "/clang++" - env["AR"] = tools_path + "/ar" - env["RANLIB"] = tools_path + "/ranlib" - env["AS"] = tools_path + "/as" + toolchain_path = ndk_root + "/toolchains/llvm/prebuilt/" + host_subpath + compiler_path = toolchain_path + "/bin" - common_opts = ["-gcc-toolchain", gcc_toolchain_path] - - # Compile flags - - env.Append(CPPFLAGS=["-isystem", env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++/include"]) - env.Append(CPPFLAGS=["-isystem", env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++abi/include"]) + env["CC"] = compiler_path + "/clang" + env["CXX"] = compiler_path + "/clang++" + env["AR"] = compiler_path + "/llvm-ar" + env["RANLIB"] = compiler_path + "/llvm-ranlib" + env["AS"] = compiler_path + "/clang" # Disable exceptions and rtti on non-tools (template) builds if env["tools"]: @@ -258,100 +166,31 @@ def configure(env): # Don't use dynamic_cast, necessary with no-rtti. env.Append(CPPDEFINES=["NO_SAFE_CAST"]) - lib_sysroot = env["ANDROID_NDK_ROOT"] + "/platforms/" + env["ndk_platform"] + "/" + env["ARCH"] - - # Using NDK unified headers (NDK r15+) - sysroot = env["ANDROID_NDK_ROOT"] + "/sysroot" - env.Append(CPPFLAGS=["--sysroot=" + sysroot]) - env.Append(CPPFLAGS=["-isystem", sysroot + "/usr/include/" + abi_subpath]) - env.Append(CPPFLAGS=["-isystem", env["ANDROID_NDK_ROOT"] + "/sources/android/support/include"]) - # For unified headers this define has to be set manually - env.Append(CPPDEFINES=[("__ANDROID_API__", str(get_platform(env["ndk_platform"])))]) - env.Append( CCFLAGS=( - "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden" - " -fno-strict-aliasing".split() + "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden -fno-strict-aliasing".split() ) ) env.Append(CPPDEFINES=["NO_STATVFS", "GLES_ENABLED"]) - if get_platform(env["ndk_platform"]) >= 24: + if get_min_sdk_version(env["ndk_platform"]) >= 24: env.Append(CPPDEFINES=[("_FILE_OFFSET_BITS", 64)]) if env["android_arch"] == "x86": - target_opts = ["-target", "i686-none-linux-android"] - # The NDK adds this if targeting API < 21, so we can drop it when Godot targets it at least + # The NDK adds this if targeting API < 24, so we can drop it when Godot targets it at least env.Append(CCFLAGS=["-mstackrealign"]) - - elif env["android_arch"] == "x86_64": - target_opts = ["-target", "x86_64-none-linux-android"] - elif env["android_arch"] == "armv7": - target_opts = ["-target", "armv7-none-linux-androideabi"] env.Append(CCFLAGS="-march=armv7-a -mfloat-abi=softfp".split()) env.Append(CPPDEFINES=["__ARM_ARCH_7__", "__ARM_ARCH_7A__"]) - # Enable ARM NEON instructions to compile more optimized code. - env.Append(CCFLAGS=["-mfpu=neon"]) env.Append(CPPDEFINES=["__ARM_NEON__"]) - elif env["android_arch"] == "arm64v8": - target_opts = ["-target", "aarch64-none-linux-android"] env.Append(CCFLAGS=["-mfix-cortex-a53-835769"]) env.Append(CPPDEFINES=["__ARM_ARCH_8A__"]) - env.Append(CCFLAGS=target_opts) - env.Append(CCFLAGS=common_opts) - # Link flags - ndk_version = get_env_ndk_version(env["ANDROID_NDK_ROOT"]) - if ndk_version != None and LooseVersion(ndk_version) >= LooseVersion("17.1.4828580"): - env.Append(LINKFLAGS=["-Wl,--exclude-libs,libgcc.a", "-Wl,--exclude-libs,libatomic.a", "-nostdlib++"]) - else: - env.Append( - LINKFLAGS=[ - env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++/libs/" + arch_subpath + "/libandroid_support.a" - ] - ) - env.Append(LINKFLAGS=["-shared", "--sysroot=" + lib_sysroot, "-Wl,--warn-shared-textrel"]) - env.Append(LIBPATH=[env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++/libs/" + arch_subpath + "/"]) - env.Append( - LINKFLAGS=[env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++/libs/" + arch_subpath + "/libc++_shared.so"] - ) - - if env["android_arch"] == "armv7": - env.Append(LINKFLAGS="-Wl,--fix-cortex-a8".split()) - env.Append(LINKFLAGS="-Wl,--no-undefined -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now".split()) - env.Append(LINKFLAGS="-Wl,-soname,libgodot_android.so -Wl,--gc-sections".split()) - - env.Append(LINKFLAGS=target_opts) - env.Append(LINKFLAGS=common_opts) - - env.Append( - LIBPATH=[ - env["ANDROID_NDK_ROOT"] - + "/toolchains/" - + target_subpath - + "/prebuilt/" - + host_subpath - + "/lib/gcc/" - + abi_subpath - + "/4.9.x" - ] - ) - env.Append( - LIBPATH=[ - env["ANDROID_NDK_ROOT"] - + "/toolchains/" - + target_subpath - + "/prebuilt/" - + host_subpath - + "/" - + abi_subpath - + "/lib" - ] - ) + env.Append(LINKFLAGS="-Wl,--gc-sections -Wl,--no-undefined -Wl,-z,now".split()) + env.Append(LINKFLAGS="-Wl,-soname,libgodot_android.so") env.Prepend(CPPPATH=["#platform/android"]) env.Append(CPPDEFINES=["ANDROID_ENABLED", "UNIX_ENABLED", "NO_FCNTL"]) @@ -361,25 +200,3 @@ def configure(env): env.Append(CPPDEFINES=["VULKAN_ENABLED"]) if not env["use_volk"]: env.Append(LIBS=["vulkan"]) - - -# Return the project NDK version. -# This is kept in sync with the value in 'platform/android/java/app/config.gradle'. -def get_project_ndk_version(): - return "21.4.7075529" - - -# Return NDK version string in source.properties (adapted from the Chromium project). -def get_env_ndk_version(path): - if path is None: - return None - prop_file_path = os.path.join(path, "source.properties") - try: - with open(prop_file_path) as prop_file: - for line in prop_file: - key_value = list(map(lambda x: x.strip(), line.split("="))) - if key_value[0] == "Pkg.Revision": - return key_value[1] - except Exception: - print("Could not read source prop file '%s'" % prop_file_path) - return None diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 7fb4f54fca..eb344d3b43 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -31,27 +31,28 @@ #include "dir_access_jandroid.h" #include "core/string/print_string.h" -#include "file_access_android.h" #include "string_android.h" #include "thread_jandroid.h" -jobject DirAccessJAndroid::io = nullptr; +jobject DirAccessJAndroid::dir_access_handler = nullptr; jclass DirAccessJAndroid::cls = nullptr; jmethodID DirAccessJAndroid::_dir_open = nullptr; jmethodID DirAccessJAndroid::_dir_next = nullptr; jmethodID DirAccessJAndroid::_dir_close = nullptr; jmethodID DirAccessJAndroid::_dir_is_dir = nullptr; - -DirAccess *DirAccessJAndroid::create_fs() { - return memnew(DirAccessJAndroid); -} +jmethodID DirAccessJAndroid::_dir_exists = nullptr; +jmethodID DirAccessJAndroid::_file_exists = nullptr; +jmethodID DirAccessJAndroid::_get_drive_count = nullptr; +jmethodID DirAccessJAndroid::_get_drive = nullptr; +jmethodID DirAccessJAndroid::_make_dir = nullptr; +jmethodID DirAccessJAndroid::_get_space_left = nullptr; +jmethodID DirAccessJAndroid::_rename = nullptr; +jmethodID DirAccessJAndroid::_remove = nullptr; +jmethodID DirAccessJAndroid::_current_is_hidden = nullptr; Error DirAccessJAndroid::list_dir_begin() { list_dir_end(); - JNIEnv *env = get_jni_env(); - - jstring js = env->NewStringUTF(current_dir.utf8().get_data()); - int res = env->CallIntMethod(io, _dir_open, js); + int res = dir_open(current_dir); if (res <= 0) { return ERR_CANT_OPEN; } @@ -63,26 +64,39 @@ Error DirAccessJAndroid::list_dir_begin() { String DirAccessJAndroid::get_next() { ERR_FAIL_COND_V(id == 0, ""); + if (_dir_next) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ""); + jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id); + if (!str) { + return ""; + } - JNIEnv *env = get_jni_env(); - jstring str = (jstring)env->CallObjectMethod(io, _dir_next, id); - if (!str) { + String ret = jstring_to_string((jstring)str, env); + env->DeleteLocalRef((jobject)str); + return ret; + } else { return ""; } - - String ret = jstring_to_string((jstring)str, env); - env->DeleteLocalRef((jobject)str); - return ret; } bool DirAccessJAndroid::current_is_dir() const { - JNIEnv *env = get_jni_env(); - - return env->CallBooleanMethod(io, _dir_is_dir, id); + if (_dir_is_dir) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, get_access_type(), id); + } else { + return false; + } } bool DirAccessJAndroid::current_is_hidden() const { - return current != "." && current != ".." && current.begins_with("."); + if (_current_is_hidden) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, get_access_type(), id); + } + return false; } void DirAccessJAndroid::list_dir_end() { @@ -90,152 +104,224 @@ void DirAccessJAndroid::list_dir_end() { return; } - JNIEnv *env = get_jni_env(); - env->CallVoidMethod(io, _dir_close, id); + dir_close(id); id = 0; } int DirAccessJAndroid::get_drive_count() { - return 0; + if (_get_drive_count) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + return env->CallIntMethod(dir_access_handler, _get_drive_count, get_access_type()); + } else { + return 0; + } } String DirAccessJAndroid::get_drive(int p_drive) { - return ""; + if (_get_drive) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ""); + jstring j_drive = (jstring)env->CallObjectMethod(dir_access_handler, _get_drive, get_access_type(), p_drive); + if (!j_drive) { + return ""; + } + + String drive = jstring_to_string(j_drive, env); + env->DeleteLocalRef(j_drive); + return drive; + } else { + return ""; + } } Error DirAccessJAndroid::change_dir(String p_dir) { - JNIEnv *env = get_jni_env(); - - if (p_dir.is_empty() || p_dir == "." || (p_dir == ".." && current_dir.is_empty())) { + String new_dir = get_absolute_path(p_dir); + if (new_dir == current_dir) { return OK; } - String new_dir; - - if (p_dir != "res://" && p_dir.length() > 1 && p_dir.ends_with("/")) { - p_dir = p_dir.substr(0, p_dir.length() - 1); - } - - if (p_dir.begins_with("/")) { - new_dir = p_dir.substr(1, p_dir.length()); - } else if (p_dir.begins_with("res://")) { - new_dir = p_dir.substr(6, p_dir.length()); - } else if (current_dir.is_empty()) { - new_dir = p_dir; - } else { - new_dir = current_dir.plus_file(p_dir); - } - - //test if newdir exists - new_dir = new_dir.simplify_path(); - - jstring js = env->NewStringUTF(new_dir.utf8().get_data()); - int res = env->CallIntMethod(io, _dir_open, js); - env->DeleteLocalRef(js); - if (res <= 0) { + if (!dir_exists(new_dir)) { return ERR_INVALID_PARAMETER; } - env->CallVoidMethod(io, _dir_close, res); - current_dir = new_dir; - return OK; } -String DirAccessJAndroid::get_current_dir(bool p_include_drive) { - return "res://" + current_dir; +String DirAccessJAndroid::get_absolute_path(String p_path) { + if (current_dir != "" && p_path == current_dir) { + return current_dir; + } + + if (p_path.is_relative_path()) { + p_path = get_current_dir().plus_file(p_path); + } + + p_path = fix_path(p_path); + p_path = p_path.simplify_path(); + return p_path; } bool DirAccessJAndroid::file_exists(String p_file) { - String sd; - if (current_dir.is_empty()) { - sd = p_file; + if (_file_exists) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + + String path = get_absolute_path(p_file); + jstring j_path = env->NewStringUTF(path.utf8().get_data()); + bool result = env->CallBooleanMethod(dir_access_handler, _file_exists, get_access_type(), j_path); + env->DeleteLocalRef(j_path); + return result; } else { - sd = current_dir.plus_file(p_file); + return false; } - - FileAccessAndroid *f = memnew(FileAccessAndroid); - bool exists = f->file_exists(sd); - memdelete(f); - - return exists; } bool DirAccessJAndroid::dir_exists(String p_dir) { - JNIEnv *env = get_jni_env(); - - String sd; - - if (current_dir.is_empty()) { - sd = p_dir; + if (_dir_exists) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + + String path = get_absolute_path(p_dir); + jstring j_path = env->NewStringUTF(path.utf8().get_data()); + bool result = env->CallBooleanMethod(dir_access_handler, _dir_exists, get_access_type(), j_path); + env->DeleteLocalRef(j_path); + return result; } else { - if (p_dir.is_relative_path()) { - sd = current_dir.plus_file(p_dir); - } else { - sd = fix_path(p_dir); - } + return false; } +} - String path = sd.simplify_path(); - - if (path.begins_with("/")) { - path = path.substr(1, path.length()); - } else if (path.begins_with("res://")) { - path = path.substr(6, path.length()); +Error DirAccessJAndroid::make_dir_recursive(String p_dir) { + // Check if the directory exists already + if (dir_exists(p_dir)) { + return ERR_ALREADY_EXISTS; } - jstring js = env->NewStringUTF(path.utf8().get_data()); - int res = env->CallIntMethod(io, _dir_open, js); - env->DeleteLocalRef(js); - if (res <= 0) { - return false; - } + if (_make_dir) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); - env->CallVoidMethod(io, _dir_close, res); - - return true; + String path = get_absolute_path(p_dir); + jstring j_dir = env->NewStringUTF(path.utf8().get_data()); + bool result = env->CallBooleanMethod(dir_access_handler, _make_dir, get_access_type(), j_dir); + env->DeleteLocalRef(j_dir); + if (result) { + return OK; + } else { + return FAILED; + } + } else { + return ERR_UNCONFIGURED; + } } Error DirAccessJAndroid::make_dir(String p_dir) { - ERR_FAIL_V(ERR_UNAVAILABLE); + return make_dir_recursive(p_dir); } Error DirAccessJAndroid::rename(String p_from, String p_to) { - ERR_FAIL_V(ERR_UNAVAILABLE); -} + if (_rename) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); -Error DirAccessJAndroid::remove(String p_name) { - ERR_FAIL_V(ERR_UNAVAILABLE); + String from_path = get_absolute_path(p_from); + jstring j_from = env->NewStringUTF(from_path.utf8().get_data()); + + String to_path = get_absolute_path(p_to); + jstring j_to = env->NewStringUTF(to_path.utf8().get_data()); + + bool result = env->CallBooleanMethod(dir_access_handler, _rename, get_access_type(), j_from, j_to); + env->DeleteLocalRef(j_from); + env->DeleteLocalRef(j_to); + if (result) { + return OK; + } else { + return FAILED; + } + } else { + return ERR_UNCONFIGURED; + } } -String DirAccessJAndroid::get_filesystem_type() const { - return "APK"; +Error DirAccessJAndroid::remove(String p_name) { + if (_remove) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); + + String path = get_absolute_path(p_name); + jstring j_name = env->NewStringUTF(path.utf8().get_data()); + bool result = env->CallBooleanMethod(dir_access_handler, _remove, get_access_type(), j_name); + env->DeleteLocalRef(j_name); + if (result) { + return OK; + } else { + return FAILED; + } + } else { + return ERR_UNCONFIGURED; + } } uint64_t DirAccessJAndroid::get_space_left() { - return 0; + if (_get_space_left) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + return env->CallLongMethod(dir_access_handler, _get_space_left, get_access_type()); + } else { + return 0; + } } -void DirAccessJAndroid::setup(jobject p_io) { +void DirAccessJAndroid::setup(jobject p_dir_access_handler) { JNIEnv *env = get_jni_env(); - io = p_io; + dir_access_handler = env->NewGlobalRef(p_dir_access_handler); - jclass c = env->GetObjectClass(io); + jclass c = env->GetObjectClass(dir_access_handler); cls = (jclass)env->NewGlobalRef(c); - _dir_open = env->GetMethodID(cls, "dir_open", "(Ljava/lang/String;)I"); - _dir_next = env->GetMethodID(cls, "dir_next", "(I)Ljava/lang/String;"); - _dir_close = env->GetMethodID(cls, "dir_close", "(I)V"); - _dir_is_dir = env->GetMethodID(cls, "dir_is_dir", "(I)Z"); - - //(*env)->CallVoidMethod(env,obj,aMethodID, myvar); + _dir_open = env->GetMethodID(cls, "dirOpen", "(ILjava/lang/String;)I"); + _dir_next = env->GetMethodID(cls, "dirNext", "(II)Ljava/lang/String;"); + _dir_close = env->GetMethodID(cls, "dirClose", "(II)V"); + _dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(II)Z"); + _dir_exists = env->GetMethodID(cls, "dirExists", "(ILjava/lang/String;)Z"); + _file_exists = env->GetMethodID(cls, "fileExists", "(ILjava/lang/String;)Z"); + _get_drive_count = env->GetMethodID(cls, "getDriveCount", "(I)I"); + _get_drive = env->GetMethodID(cls, "getDrive", "(II)Ljava/lang/String;"); + _make_dir = env->GetMethodID(cls, "makeDir", "(ILjava/lang/String;)Z"); + _get_space_left = env->GetMethodID(cls, "getSpaceLeft", "(I)J"); + _rename = env->GetMethodID(cls, "rename", "(ILjava/lang/String;Ljava/lang/String;)Z"); + _remove = env->GetMethodID(cls, "remove", "(ILjava/lang/String;)Z"); + _current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(II)Z"); } DirAccessJAndroid::DirAccessJAndroid() { - id = 0; } DirAccessJAndroid::~DirAccessJAndroid() { list_dir_end(); } + +int DirAccessJAndroid::dir_open(String p_path) { + if (_dir_open) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + + String path = get_absolute_path(p_path); + jstring js = env->NewStringUTF(path.utf8().get_data()); + int dirId = env->CallIntMethod(dir_access_handler, _dir_open, get_access_type(), js); + env->DeleteLocalRef(js); + return dirId; + } else { + return 0; + } +} + +void DirAccessJAndroid::dir_close(int p_id) { + if (_dir_close) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(dir_access_handler, _dir_close, get_access_type(), p_id); + } +} diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index 4f4a984b12..d469c9d317 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -32,60 +32,70 @@ #define DIR_ACCESS_JANDROID_H #include "core/io/dir_access.h" +#include "drivers/unix/dir_access_unix.h" #include "java_godot_lib_jni.h" #include <stdio.h> -class DirAccessJAndroid : public DirAccess { - //AAssetDir* aad; - - static jobject io; +/// Android implementation of the DirAccess interface used to provide access to +/// ACCESS_FILESYSTEM and ACCESS_RESOURCES directory resources. +/// The implementation use jni in order to comply with Android filesystem +/// access restriction. +class DirAccessJAndroid : public DirAccessUnix { + static jobject dir_access_handler; static jclass cls; static jmethodID _dir_open; static jmethodID _dir_next; static jmethodID _dir_close; static jmethodID _dir_is_dir; - - int id; - - String current_dir; - String current; - - static DirAccess *create_fs(); + static jmethodID _dir_exists; + static jmethodID _file_exists; + static jmethodID _get_drive_count; + static jmethodID _get_drive; + static jmethodID _make_dir; + static jmethodID _get_space_left; + static jmethodID _rename; + static jmethodID _remove; + static jmethodID _current_is_hidden; public: - virtual Error list_dir_begin(); ///< This starts dir listing - virtual String get_next(); - virtual bool current_is_dir() const; - virtual bool current_is_hidden() const; - virtual void list_dir_end(); ///< + virtual Error list_dir_begin() override; ///< This starts dir listing + virtual String get_next() override; + virtual bool current_is_dir() const override; + virtual bool current_is_hidden() const override; + virtual void list_dir_end() override; ///< - virtual int get_drive_count(); - virtual String get_drive(int p_drive); + virtual int get_drive_count() override; + virtual String get_drive(int p_drive) override; - 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 Error change_dir(String p_dir) override; ///< can be relative or absolute, return false on success - virtual bool file_exists(String p_file); - virtual bool dir_exists(String p_dir); + virtual bool file_exists(String p_file) override; + virtual bool dir_exists(String p_dir) override; - virtual Error make_dir(String p_dir); + virtual Error make_dir(String p_dir) override; + virtual Error make_dir_recursive(String p_dir) override; - virtual Error rename(String p_from, String p_to); - virtual Error remove(String p_name); + virtual Error rename(String p_from, String p_to) override; + virtual Error remove(String p_name) override; - virtual bool is_link(String p_file) { return false; } - virtual String read_link(String p_file) { return p_file; } - virtual Error create_link(String p_source, String p_target) { return FAILED; } + virtual bool is_link(String p_file) override { return false; } + virtual String read_link(String p_file) override { return p_file; } + virtual Error create_link(String p_source, String p_target) override { return FAILED; } - virtual String get_filesystem_type() const; + virtual uint64_t get_space_left() override; - uint64_t get_space_left(); - - static void setup(jobject p_io); + static void setup(jobject p_dir_access_handler); DirAccessJAndroid(); ~DirAccessJAndroid(); + +private: + int id = 0; + + int dir_open(String p_path); + void dir_close(int p_id); + String get_absolute_path(String p_path); }; #endif // DIR_ACCESS_JANDROID_H diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index a7a8801bdc..3be220d110 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,9 +75,37 @@ 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); + ERR_FAIL_NULL(godot_java); if (godot_java->has_set_clipboard()) { godot_java->set_clipboard(p_text); @@ -86,7 +116,7 @@ void DisplayServerAndroid::clipboard_set(const String &p_text) { String DisplayServerAndroid::clipboard_get() const { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); - ERR_FAIL_COND_V(!godot_java, String()); + ERR_FAIL_NULL_V(godot_java, String()); if (godot_java->has_get_clipboard()) { return godot_java->get_clipboard(); @@ -97,7 +127,7 @@ String DisplayServerAndroid::clipboard_get() const { bool DisplayServerAndroid::clipboard_has() const { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); - ERR_FAIL_COND_V(!godot_java, false); + ERR_FAIL_NULL_V(godot_java, false); if (godot_java->has_has_clipboard()) { return godot_java->has_clipboard(); @@ -106,9 +136,21 @@ bool DisplayServerAndroid::clipboard_has() const { } } +Array DisplayServerAndroid::get_display_cutouts() const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_NULL_V(godot_io_java, Array()); + return godot_io_java->get_display_cutouts(); +} + +Rect2i DisplayServerAndroid::get_display_safe_area() const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_NULL_V(godot_io_java, Rect2i()); + return godot_io_java->get_display_safe_area(); +} + void DisplayServerAndroid::screen_set_keep_on(bool p_enable) { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); - ERR_FAIL_COND(!godot_java); + ERR_FAIL_NULL(godot_java); godot_java->set_keep_screen_on(p_enable); keep_screen_on = p_enable; @@ -120,14 +162,14 @@ bool DisplayServerAndroid::screen_is_kept_on() const { void DisplayServerAndroid::screen_set_orientation(DisplayServer::ScreenOrientation p_orientation, int p_screen) { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND(!godot_io_java); + ERR_FAIL_NULL(godot_io_java); godot_io_java->set_screen_orientation(p_orientation); } DisplayServer::ScreenOrientation DisplayServerAndroid::screen_get_orientation(int p_screen) const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND_V(!godot_io_java, SCREEN_LANDSCAPE); + ERR_FAIL_NULL_V(godot_io_java, SCREEN_LANDSCAPE); const int orientation = godot_io_java->get_screen_orientation(); ERR_FAIL_INDEX_V_MSG(orientation, 7, SCREEN_LANDSCAPE, "Unrecognized screen orientation"); @@ -147,20 +189,24 @@ Size2i DisplayServerAndroid::screen_get_size(int p_screen) const { } Rect2i DisplayServerAndroid::screen_get_usable_rect(int p_screen) const { - GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND_V(!godot_io_java, Rect2i()); - int xywh[4]; - godot_io_java->screen_get_usable_rect(xywh); - return Rect2i(xywh[0], xywh[1], xywh[2], xywh[3]); + Size2i display_size = OS_Android::get_singleton()->get_display_size(); + return Rect2i(0, 0, display_size.width, display_size.height); } int DisplayServerAndroid::screen_get_dpi(int p_screen) const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND_V(!godot_io_java, 0); + ERR_FAIL_NULL_V(godot_io_java, 0); 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_NULL_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) { @@ -177,7 +223,7 @@ bool DisplayServerAndroid::screen_is_touchscreen(int p_screen) const { void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_length, int p_cursor_start, int p_cursor_end) { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND(!godot_io_java); + ERR_FAIL_NULL(godot_io_java); if (godot_io_java->has_vk()) { godot_io_java->show_vk(p_existing_text, p_multiline, p_max_length, p_cursor_start, p_cursor_end); @@ -188,7 +234,7 @@ void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text, void DisplayServerAndroid::virtual_keyboard_hide() { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND(!godot_io_java); + ERR_FAIL_NULL(godot_io_java); if (godot_io_java->has_vk()) { godot_io_java->hide_vk(); @@ -199,7 +245,7 @@ void DisplayServerAndroid::virtual_keyboard_hide() { int DisplayServerAndroid::virtual_keyboard_get_height() const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND_V(!godot_io_java, 0); + ERR_FAIL_NULL_V(godot_io_java, 0); return godot_io_java->get_vk_height(); } @@ -270,7 +316,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. @@ -410,9 +456,9 @@ void DisplayServerAndroid::reset_window() { #if defined(VULKAN_ENABLED) if (rendering_driver == "vulkan") { ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window(); - ERR_FAIL_COND(!native_window); + ERR_FAIL_NULL(native_window); - ERR_FAIL_COND(!context_vulkan); + ERR_FAIL_NULL(context_vulkan); VSyncMode last_vsync_mode = context_vulkan->get_vsync_mode(MAIN_WINDOW_ID); context_vulkan->window_destroy(MAIN_WINDOW_ID); @@ -473,7 +519,7 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis if (rendering_driver == "vulkan") { ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window(); - ERR_FAIL_COND(!native_window); + ERR_FAIL_NULL(native_window); context_vulkan = memnew(VulkanContextAndroid); if (context_vulkan->initialize() != OK) { diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index 23077a6529..65bf2ec1a8 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -91,10 +91,22 @@ 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; + virtual Array get_display_cutouts() const override; + virtual Rect2i get_display_safe_area() const override; + virtual void screen_set_keep_on(bool p_enable) override; virtual bool screen_is_kept_on() const override; @@ -106,6 +118,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.cpp b/platform/android/export/export.cpp index aa4b394965..5bbe0ffab6 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -30,17 +30,12 @@ #include "export.h" -#include "export_plugin.h" - #include "core/os/os.h" #include "editor/editor_settings.h" +#include "editor/export/editor_export.h" +#include "export_plugin.h" void register_android_exporter() { - String exe_ext; - if (OS::get_singleton()->get_name() == "Windows") { - exe_ext = "*.exe"; - } - EDITOR_DEF("export/android/android_sdk_path", ""); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); EDITOR_DEF("export/android/debug_keystore", ""); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 2c431028b0..2cfb152804 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -123,6 +123,7 @@ static const char *android_perms[] = { "MANAGE_ACCOUNTS", "MANAGE_APP_TOKENS", "MANAGE_DOCUMENTS", + "MANAGE_EXTERNAL_STORAGE", "MASTER_CLEAR", "MEDIA_CONTENT_CONTROL", "MODIFY_AUDIO_SETTINGS", @@ -207,9 +208,9 @@ static const char *SPLASH_CONFIG_PATH = "res://android/build/res/drawable/splash static const char *GDNATIVE_LIBS_PATH = "res://android/build/libs/gdnativelibs.json"; static const int icon_densities_count = 6; -static const char *launcher_icon_option = "launcher_icons/main_192x192"; -static const char *launcher_adaptive_icon_foreground_option = "launcher_icons/adaptive_foreground_432x432"; -static const char *launcher_adaptive_icon_background_option = "launcher_icons/adaptive_background_432x432"; +static const char *launcher_icon_option = PNAME("launcher_icons/main_192x192"); +static const char *launcher_adaptive_icon_foreground_option = PNAME("launcher_icons/adaptive_foreground_432x432"); +static const char *launcher_adaptive_icon_background_option = PNAME("launcher_icons/adaptive_background_432x432"); static const LauncherIcon launcher_icons[icon_densities_count] = { { "res/mipmap-xxxhdpi-v4/icon.png", 192 }, @@ -245,11 +246,10 @@ static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets"; static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets"; static const int DEFAULT_MIN_SDK_VERSION = 19; // Should match the value in 'platform/android/java/app/config.gradle#minSdk' -static const int DEFAULT_TARGET_SDK_VERSION = 30; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' -const String SDK_VERSION_RANGE = vformat("%s,%s,1", DEFAULT_MIN_SDK_VERSION, DEFAULT_TARGET_SDK_VERSION); +static const int DEFAULT_TARGET_SDK_VERSION = 32; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' 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 @@ -277,6 +277,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } } +#ifndef ANDROID_ENABLED // Check for devices updates String adb = get_adb_path(); if (FileAccess::exists(adb)) { @@ -388,6 +389,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { ea->devices_changed.set(); } } +#endif uint64_t sleep = 200; uint64_t wait = 3000000; @@ -400,6 +402,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } } +#ifndef ANDROID_ENABLED if (EditorSettings::get_singleton()->get("export/android/shutdown_adb_on_exit")) { String adb = get_adb_path(); if (!FileAccess::exists(adb)) { @@ -409,7 +412,8 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { List<String> args; args.push_back("kill-server"); OS::get_singleton()->execute(adb, args); - }; + } +#endif } String EditorExportPlatformAndroid::get_project_name(const String &p_name) const { @@ -522,8 +526,8 @@ bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package, bool EditorExportPlatformAndroid::_should_compress_asset(const String &p_path, const Vector<uint8_t> &p_data) { /* - * By not compressing files with little or not benefit in doing so, - * a performance gain is expected attime. Moreover, if the APK is + * By not compressing files with little or no benefit in doing so, + * a performance gain is expected at runtime. Moreover, if the APK is * zip-aligned, assets stored as they are can be efficiently read by * Android by memory-mapping them. */ @@ -543,7 +547,7 @@ bool EditorExportPlatformAndroid::_should_compress_asset(const String &p_path, c ".webp", // Same reasoning as .png ".cfb", // Don't let small config files slow-down startup ".scn", // Binary scenes are usually already compressed - ".stex", // Streamable textures are usually already compressed + ".ctex", // Streamable textures are usually already compressed // Trailer for easier processing nullptr }; @@ -596,8 +600,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 +689,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 +714,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 +729,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]); @@ -748,10 +752,14 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared return OK; } -bool EditorExportPlatformAndroid::_has_storage_permission(const Vector<String> &p_permissions) { +bool EditorExportPlatformAndroid::_has_read_write_storage_permission(const Vector<String> &p_permissions) { return p_permissions.find("android.permission.READ_EXTERNAL_STORAGE") != -1 || p_permissions.find("android.permission.WRITE_EXTERNAL_STORAGE") != -1; } +bool EditorExportPlatformAndroid::_has_manage_external_storage_permission(const Vector<String> &p_permissions) { + return p_permissions.find("android.permission.MANAGE_EXTERNAL_STORAGE") != -1; +} + void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions) { const char **aperms = android_perms; while (*aperms) { @@ -799,7 +807,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres _get_permissions(p_preset, p_give_internet, perms); for (int i = 0; i < perms.size(); i++) { String permission = perms.get(i); - if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || permission == "android.permission.READ_EXTERNAL_STORAGE") { + if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || (permission == "android.permission.READ_EXTERNAL_STORAGE" && _has_manage_external_storage_permission(perms))) { manifest_text += vformat(" <uses-permission android:name=\"%s\" android:maxSdkVersion=\"29\" />\n", permission); } else { manifest_text += vformat(" <uses-permission android:name=\"%s\" />\n", permission); @@ -807,8 +815,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres } manifest_text += _get_xr_features_tag(p_preset); - manifest_text += _get_instrumentation_tag(p_preset); - manifest_text += _get_application_tag(p_preset, _has_storage_permission(perms)); + manifest_text += _get_application_tag(p_preset, _has_read_write_storage_permission(perms)); manifest_text += "</manifest>\n"; String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release")); @@ -834,11 +841,9 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p uint32_t ofs = 8; uint32_t string_count = 0; - //uint32_t styles_count = 0; uint32_t string_flags = 0; uint32_t string_data_offset = 0; - //uint32_t styles_offset = 0; uint32_t string_table_begins = 0; uint32_t string_table_ends = 0; Vector<uint8_t> stable_extra; @@ -863,11 +868,12 @@ 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. _get_permissions(p_preset, p_give_internet, perms); - bool has_storage_permission = _has_storage_permission(perms); + bool has_read_write_storage_permission = _has_read_write_storage_permission(perms); while (ofs < (uint32_t)p_manifest.size()) { uint32_t chunk = decode_uint32(&p_manifest[ofs]); @@ -878,10 +884,8 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p int iofs = ofs + 8; string_count = decode_uint32(&p_manifest[iofs]); - // iofs + 4 is `styles_count`. string_flags = decode_uint32(&p_manifest[iofs + 8]); string_data_offset = decode_uint32(&p_manifest[iofs + 12]); - // iofs + 16 is `styles_offset`. uint32_t st_offset = iofs + 20; string_table.resize(string_count); @@ -953,7 +957,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p } if (tname == "application" && attrname == "requestLegacyExternalStorage") { - encode_uint32(has_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]); + encode_uint32(has_read_write_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]); } if (tname == "application" && attrname == "allowBackup") { @@ -968,10 +972,6 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p encode_uint32(retain_data_on_uninstall, &p_manifest.write[iofs + 16]); } - if (tname == "instrumentation" && attrname == "targetPackage") { - string_table.write[attr_value] = get_package_name(package_name); - } - if (tname == "activity" && attrname == "screenOrientation") { encode_uint32(screen_orientation, &p_manifest.write[iofs + 16]); } @@ -980,6 +980,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]); @@ -995,16 +999,23 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p } } - if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_metadata_name") { - if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + // Hand tracking related configurations + if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_metadata_name") { string_table.write[attr_value] = "com.oculus.handtracking.frequency"; } - } - if (tname == "meta-data" && attrname == "value" && value == "xr_hand_tracking_metadata_value") { - if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + if (tname == "meta-data" && attrname == "value" && value == "xr_hand_tracking_metadata_value") { string_table.write[attr_value] = (hand_tracking_frequency_index == XR_HAND_TRACKING_FREQUENCY_LOW ? "LOW" : "HIGH"); } + + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_version_name") { + string_table.write[attr_value] = "com.oculus.handtracking.version"; + } + + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_version_value") { + string_table.write[attr_value] = "V2.0"; + } } iofs += 20; @@ -1387,6 +1398,7 @@ void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> & Vector<String> string_table; String package_name = p_preset->get("package/name"); + Dictionary appnames = ProjectSettings::get_singleton()->get("application/config/name_localized"); for (uint32_t i = 0; i < string_count; i++) { uint32_t offset = decode_uint32(&r_manifest[string_table_begins + i * 4]); @@ -1401,9 +1413,8 @@ void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> & } else { String lang = str.substr(str.rfind("-") + 1, str.length()).replace("-", "_"); - String prop = "application/config/name_" + lang; - if (ProjectSettings::get_singleton()->has_setting(prop)) { - str = ProjectSettings::get_singleton()->get(prop); + if (appnames.has(lang)) { + str = appnames[lang]; } else { str = get_project_name(package_name); } @@ -1679,13 +1690,18 @@ void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPres void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_options) { r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "custom_template/use_custom_build"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "custom_template/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK)); + + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "custom_build/use_custom_build"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "custom_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK)); + // Using String instead of int to default to an empty string (no override) with placeholder for instructions (see GH-62465). + // This implies doing validation that the string is a proper int. + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_MIN_SDK_VERSION)), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "")); Vector<PluginConfigAndroid> plugins_configs = get_plugins(); for (int i = 0; i < plugins_configs.size(); i++) { print_verbose("Found Android plugin " + plugins_configs[i].name); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "plugins/" + plugins_configs[i].name), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), plugins_configs[i].name)), false)); } plugins_changed.clear(); @@ -1695,7 +1711,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio // All Android devices supporting Vulkan run 64-bit Android, // so there is usually no point in exporting for 32-bit Android. const bool is_default = abi == "arm64-v8a"; - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "architectures/" + abi), is_default)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("architectures"), abi)), is_default)); } r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"), "")); @@ -1707,8 +1723,6 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name"), "1.0")); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/min_sdk", PROPERTY_HINT_RANGE, SDK_VERSION_RANGE), DEFAULT_MIN_SDK_VERSION)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/target_sdk", PROPERTY_HINT_RANGE, SDK_VERSION_RANGE), DEFAULT_TARGET_SDK_VERSION)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "org.godotengine.$genname")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), "")); @@ -1746,7 +1760,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio const char **perms = android_perms; while (*perms) { - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "permissions/" + String(*perms).to_lower()), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("permissions"), String(*perms).to_lower())), false)); perms++; } } @@ -1815,7 +1829,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, String can_export_error; bool can_export_missing_templates; if (!can_export(p_preset, can_export_error, can_export_missing_templates)) { - EditorNode::add_io_error(can_export_error); + add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error); return ERR_UNCONFIGURED; } @@ -1843,7 +1857,8 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, { \ DirAccess::remove_file_or_error(tmp_export_path); \ return m_err; \ - } + } \ + ((void)0) // Export to temporary APK before sending to device. Error err = export_project_helper(p_preset, true, tmp_export_path, EXPORT_FORMAT_APK, true, p_debug_flags); @@ -1893,7 +1908,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, err = OS::get_singleton()->execute(adb, args, &output, &rv, true); print_verbose(output); if (err || rv != 0) { - EditorNode::add_io_error(vformat(TTR("Could not install to device: %s"), output)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not install to device: %s"), output)); CLEANUP_AND_RETURN(ERR_CANT_CREATE); } @@ -1971,7 +1986,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, err = OS::get_singleton()->execute(adb, args, &output, &rv, true); print_verbose(output); if (err || rv != 0) { - EditorNode::add_io_error(TTR("Could not execute on device.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device.")); CLEANUP_AND_RETURN(ERR_CANT_CREATE); } @@ -2003,7 +2018,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; @@ -2026,7 +2041,7 @@ String EditorExportPlatformAndroid::get_apksigner_path() { da->list_dir_end(); if (apksigner_path.is_empty()) { - EditorNode::get_singleton()->show_warning(TTR("Unable to find the 'apksigner' tool.")); + print_error("Unable to find the 'apksigner' tool."); } return apksigner_path; @@ -2035,7 +2050,7 @@ String EditorExportPlatformAndroid::get_apksigner_path() { bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { String err; bool valid = false; - const bool custom_build_enabled = p_preset->get("custom_template/use_custom_build"); + const bool custom_build_enabled = p_preset->get("custom_build/use_custom_build"); // Look for export templates (first official, and if defined custom templates). @@ -2120,7 +2135,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 +2153,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!"); @@ -2197,43 +2212,73 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr if (xr_mode_index != XR_MODE_OPENXR) { if (hand_tracking > XR_HAND_TRACKING_NONE) { valid = false; - err += TTR("\"Hand Tracking\" is only valid when \"Xr Mode\" is \"OpenXR\"."); + err += TTR("\"Hand Tracking\" is only valid when \"XR Mode\" is \"OpenXR\"."); err += "\n"; } if (passthrough_mode > XR_PASSTHROUGH_NONE) { valid = false; - err += TTR("\"Passthrough\" is only valid when \"Xr Mode\" is \"OpenXR\"."); + err += TTR("\"Passthrough\" is only valid when \"XR Mode\" is \"OpenXR\"."); err += "\n"; } } - if (int(p_preset->get("custom_template/export_format")) == EXPORT_FORMAT_AAB && + if (int(p_preset->get("custom_build/export_format")) == EXPORT_FORMAT_AAB && !custom_build_enabled) { valid = false; err += TTR("\"Export AAB\" is only valid when \"Use Custom Build\" is enabled."); err += "\n"; } - // Check the min sdk version - int min_sdk_version = p_preset->get("version/min_sdk"); - if (min_sdk_version != DEFAULT_MIN_SDK_VERSION && !custom_build_enabled) { - valid = false; - err += TTR("Changing the \"Min Sdk\" is only valid when \"Use Custom Build\" is enabled."); - err += "\n"; + // Check the min sdk version. + String min_sdk_str = p_preset->get("custom_build/min_sdk"); + int min_sdk_int = DEFAULT_MIN_SDK_VERSION; + if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do. + if (!custom_build_enabled) { + valid = false; + err += TTR("\"Min SDK\" can only be overridden when \"Use Custom Build\" is enabled."); + err += "\n"; + } + if (!min_sdk_str.is_valid_int()) { + valid = false; + err += vformat(TTR("\"Min SDK\" should be a valid integer, but got \"%s\" which is invalid."), min_sdk_str); + err += "\n"; + } else { + min_sdk_int = min_sdk_str.to_int(); + if (min_sdk_int < DEFAULT_MIN_SDK_VERSION) { + valid = false; + err += vformat(TTR("\"Min SDK\" cannot be lower than %d, which is the version needed by the Godot library."), DEFAULT_MIN_SDK_VERSION); + err += "\n"; + } + } } - // Check the target sdk version - int target_sdk_version = p_preset->get("version/target_sdk"); - if (target_sdk_version != DEFAULT_TARGET_SDK_VERSION && !custom_build_enabled) { - valid = false; - err += TTR("Changing the \"Target Sdk\" is only valid when \"Use Custom Build\" is enabled."); - err += "\n"; + // Check the target sdk version. + String target_sdk_str = p_preset->get("custom_build/target_sdk"); + int target_sdk_int = DEFAULT_TARGET_SDK_VERSION; + if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do. + if (!custom_build_enabled) { + valid = false; + err += TTR("\"Target SDK\" can only be overridden when \"Use Custom Build\" is enabled."); + err += "\n"; + } + if (!target_sdk_str.is_valid_int()) { + valid = false; + err += vformat(TTR("\"Target SDK\" should be a valid integer, but got \"%s\" which is invalid."), target_sdk_str); + err += "\n"; + } else { + target_sdk_int = target_sdk_str.to_int(); + if (target_sdk_int > DEFAULT_TARGET_SDK_VERSION) { + // Warning only, so don't override `valid`. + err += vformat(TTR("\"Target SDK\" %d is higher than the default version %d. This may work, but wasn't tested and may be unstable."), target_sdk_int, DEFAULT_TARGET_SDK_VERSION); + err += "\n"; + } + } } - if (target_sdk_version < min_sdk_version) { + if (target_sdk_int < min_sdk_int) { valid = false; - err += TTR("\"Target Sdk\" version must be greater or equal to \"Min Sdk\" version."); + err += TTR("\"Target SDK\" version must be greater or equal to \"Min SDK\" version."); err += "\n"; } @@ -2256,9 +2301,9 @@ String EditorExportPlatformAndroid::get_apk_expansion_fullpath(const Ref<EditorE return fullpath; } -Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, const String &p_path) { +Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) { String fullpath = get_apk_expansion_fullpath(p_preset, p_path); - Error err = save_pack(p_preset, fullpath); + Error err = save_pack(p_preset, p_debug, fullpath); return err; } @@ -2322,7 +2367,7 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP } Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) { - int export_format = int(p_preset->get("custom_template/export_format")); + int export_format = int(p_preset->get("custom_build/export_format")); String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK"; String release_keystore = p_preset->get("keystore/release"); String release_username = p_preset->get("keystore/release_user"); @@ -2331,7 +2376,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre String apksigner = get_apksigner_path(); print_verbose("Starting signing of the " + export_label + " binary using " + apksigner); if (!FileAccess::exists(apksigner)) { - EditorNode::add_io_error(vformat(TTR("'apksigner' could not be found.\nPlease check the command is available in the Android SDK build-tools directory.\nThe resulting %s is unsigned."), export_label)); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting %s is unsigned."), export_label)); return OK; } @@ -2364,7 +2409,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } if (!FileAccess::exists(keystore)) { - EditorNode::add_io_error(TTR("Could not find keystore, unable to export.")); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find keystore, unable to export.")); return ERR_FILE_CANT_OPEN; } @@ -2385,10 +2430,14 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } int retval; output.clear(); - OS::get_singleton()->execute(apksigner, args, &output, &retval, true); + Error err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable.")); + return err; + } print_verbose(output); if (retval) { - EditorNode::add_io_error(vformat(TTR("'apksigner' returned with error #%d"), retval)); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' returned with error #%d"), retval)); return ERR_CANT_CREATE; } @@ -2405,10 +2454,14 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } output.clear(); - OS::get_singleton()->execute(apksigner, args, &output, &retval, true); + err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable.")); + return err; + } print_verbose(output); if (retval) { - EditorNode::add_io_error(vformat(TTR("'apksigner' verification of %s failed."), export_label)); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' verification of %s failed."), export_label)); return ERR_CANT_CREATE; } @@ -2417,12 +2470,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 +2483,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 +2503,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]); @@ -2470,7 +2523,7 @@ String EditorExportPlatformAndroid::join_list(List<String> parts, const String & } Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { - int export_format = int(p_preset->get("custom_template/export_format")); + int export_format = int(p_preset->get("custom_build/export_format")); bool should_sign = p_preset->get("package/signed"); return export_project_helper(p_preset, p_debug, p_path, export_format, should_sign, p_flags); } @@ -2483,7 +2536,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP EditorProgress ep("export", TTR("Exporting for Android"), 105, true); - bool use_custom_build = bool(p_preset->get("custom_template/use_custom_build")); + bool use_custom_build = bool(p_preset->get("custom_build/use_custom_build")); bool p_give_internet = p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG); bool apk_expansion = p_preset->get("apk_expansion/enable"); Vector<String> enabled_abis = get_enabled_abis(p_preset); @@ -2516,22 +2569,21 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP if (export_format == EXPORT_FORMAT_AAB) { if (!p_path.ends_with(".aab")) { - EditorNode::get_singleton()->show_warning(TTR("Invalid filename! Android App Bundle requires the *.aab extension.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android App Bundle requires the *.aab extension.")); return ERR_UNCONFIGURED; } if (apk_expansion) { - EditorNode::get_singleton()->show_warning(TTR("APK Expansion not compatible with Android App Bundle.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("APK Expansion not compatible with Android App Bundle.")); return ERR_UNCONFIGURED; } } if (export_format == EXPORT_FORMAT_APK && !p_path.ends_with(".apk")) { - EditorNode::get_singleton()->show_warning( - TTR("Invalid filename! Android APK requires the *.apk extension.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android APK requires the *.apk extension.")); return ERR_UNCONFIGURED; } if (export_format > EXPORT_FORMAT_AAB || export_format < EXPORT_FORMAT_APK) { - EditorNode::add_io_error(TTR("Unsupported export format!\n")); - return ERR_UNCONFIGURED; //TODO: is this the right error? + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!")); + return ERR_UNCONFIGURED; } if (use_custom_build) { @@ -2539,16 +2591,15 @@ 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) { - 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.")); + Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), 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)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Android build version mismatch: Template installed: %s, Godot version: %s. Please reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG)); return ERR_UNCONFIGURED; } } @@ -2561,7 +2612,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String project_name = get_project_name(p_preset->get("package/name")); err = _create_project_name_strings_files(p_preset, project_name); //project name localization. if (err != OK) { - EditorNode::add_io_error(TTR("Unable to overwrite res://android/build/res/*.xml files with project name")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res://android/build/res/*.xml files with project name.")); } // Copies the project icon files into the appropriate Gradle project directory. _copy_icons_to_gradle_project(p_preset, processed_splash_config_xml, splash_image, splash_bg_color_image, main_image, foreground, background); @@ -2576,22 +2627,21 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP CustomExportData user_data; user_data.assets_directory = assets_directory; user_data.debug = p_debug; - err = export_project_files(p_preset, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); + err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); if (err != OK) { - EditorNode::add_io_error(TTR("Could not export project files to gradle project\n")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project.")); 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.."); - err = save_apk_expansion_file(p_preset, p_path); + err = save_apk_expansion_file(p_preset, p_debug, p_path); if (err != OK) { - EditorNode::add_io_error(TTR("Could not write expansion package file!")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!")); return err; } } @@ -2614,8 +2664,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String package_name = get_package_name(p_preset->get("package/unique_name")); String version_code = itos(p_preset->get("version/code")); String version_name = p_preset->get("version/name"); - String min_sdk_version = itos(p_preset->get("version/min_sdk")); - String target_sdk_version = itos(p_preset->get("version/target_sdk")); + String min_sdk_version = p_preset->get("custom_build/min_sdk"); + if (!min_sdk_version.is_valid_int()) { + min_sdk_version = itos(DEFAULT_MIN_SDK_VERSION); + } + String target_sdk_version = p_preset->get("custom_build/target_sdk"); + if (!target_sdk_version.is_valid_int()) { + target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION); + } String enabled_abi_string = String("|").join(enabled_abis); String sign_flag = should_sign ? "true" : "false"; String zipalign_flag = "true"; @@ -2676,7 +2732,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP debug_keystore = OS::get_singleton()->get_resource_dir().plus_file(debug_keystore).simplify_path(); } if (!FileAccess::exists(debug_keystore)) { - EditorNode::add_io_error(TTR("Could not find keystore, unable to export.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find keystore, unable to export.")); return ERR_FILE_CANT_OPEN; } @@ -2692,7 +2748,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP release_keystore = OS::get_singleton()->get_resource_dir().plus_file(release_keystore).simplify_path(); } if (!FileAccess::exists(release_keystore)) { - EditorNode::add_io_error(TTR("Could not find keystore, unable to export.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find keystore, unable to export.")); return ERR_FILE_CANT_OPEN; } @@ -2704,7 +2760,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline); if (result != 0) { - EditorNode::get_singleton()->show_warning(TTR("Building of Android project failed, check output for the error.\nAlternatively visit docs.godotengine.org for Android build documentation.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error. Alternatively visit docs.godotengine.org for Android build documentation.")); return ERR_CANT_CREATE; } @@ -2734,7 +2790,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" "))); int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args); if (copy_result != 0) { - EditorNode::get_singleton()->show_warning(TTR("Unable to copy and rename export file, check gradle project directory for outputs.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file, check gradle project directory for outputs.")); return ERR_CANT_CREATE; } @@ -2756,7 +2812,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP src_apk = find_export_template("android_release.apk"); } if (src_apk.is_empty()) { - EditorNode::add_io_error(vformat(TTR("Package not found: %s"), src_apk)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Package not found: \"%s\"."), src_apk)); return ERR_FILE_NOT_FOUND; } } @@ -2765,8 +2821,8 @@ 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); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); if (ep.step(TTR("Creating APK..."), 0)) { return ERR_SKIP; @@ -2774,15 +2830,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP unzFile pkg = unzOpen2(src_apk.utf8().get_data(), &io); if (!pkg) { - EditorNode::add_io_error(vformat(TTR("Could not find template APK to export:\n%s"), src_apk)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not find template APK to export: \"%s\"."), src_apk)); return ERR_FILE_NOT_FOUND; } int ret = unzGoToFirstFile(pkg); - zlib_filefunc_def io2 = io; - FileAccess *dst_f = nullptr; - io2.opaque = &dst_f; + Ref<FileAccess> io2_fa; + zlib_filefunc_def io2 = zipio_create_io(&io2_fa); String tmp_unaligned_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk"); @@ -2790,7 +2845,8 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP { \ DirAccess::remove_file_or_error(tmp_unaligned_path); \ return m_err; \ - } + } \ + ((void)0) zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2); @@ -2807,6 +2863,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; @@ -2902,7 +2961,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP if (!invalid_abis.is_empty()) { String unsupported_arch = String(", ").join(invalid_abis); - EditorNode::add_io_error(vformat(TTR("Missing libraries in the export template for the selected architectures: %s.\nPlease build a template with all required libraries, or uncheck the missing architectures in the export preset."), unsupported_arch)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Missing libraries in the export template for the selected architectures: %s. Please build a template with all required libraries, or uncheck the missing architectures in the export preset."), unsupported_arch)); CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND); } @@ -2915,25 +2974,25 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP APKExportData ed; ed.ep = &ep; ed.apk = unaligned_apk; - err = export_project_files(p_preset, ignore_apk_file, &ed, save_apk_so); + err = export_project_files(p_preset, p_debug, ignore_apk_file, &ed, save_apk_so); } else { if (apk_expansion) { - err = save_apk_expansion_file(p_preset, p_path); + err = save_apk_expansion_file(p_preset, p_debug, p_path); if (err != OK) { - EditorNode::add_io_error(TTR("Could not write expansion package file!")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!")); return err; } } else { APKExportData ed; ed.ep = &ep; ed.apk = unaligned_apk; - err = export_project_files(p_preset, save_apk_file, &ed, save_apk_so); + err = export_project_files(p_preset, p_debug, save_apk_file, &ed, save_apk_so); } } if (err != OK) { unzClose(pkg); - EditorNode::add_io_error(TTR("Could not export project files")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not export project files."))); CLEANUP_AND_RETURN(ERR_SKIP); } @@ -2969,15 +3028,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP unzFile tmp_unaligned = unzOpen2(tmp_unaligned_path.utf8().get_data(), &io); if (!tmp_unaligned) { - EditorNode::add_io_error(TTR("Could not unzip temporary unaligned APK.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not unzip temporary unaligned APK."))); CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND); } ret = unzGoToFirstFile(tmp_unaligned); - io2 = io; - dst_f = nullptr; - io2.opaque = &dst_f; + io2 = zipio_create_io(&io2_fa); 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 +3048,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); @@ -3054,17 +3114,12 @@ void EditorExportPlatformAndroid::get_platform_features(List<String> *r_features r_features->push_back("android"); } -void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) { +void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) { } EditorExportPlatformAndroid::EditorExportPlatformAndroid() { - Ref<Image> img = memnew(Image(_android_logo)); - logo.instantiate(); - logo->create_from_image(img); - - img = Ref<Image>(memnew(Image(_android_run_icon))); - run_icon.instantiate(); - run_icon->create_from_image(img); + logo = ImageTexture::create_from_image(memnew(Image(_android_logo))); + run_icon = ImageTexture::create_from_image(memnew(Image(_android_run_icon))); devices_changed.set(); plugins_changed.set(); diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index a4eb608b19..8fd2f0680d 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -28,11 +28,14 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#ifndef ANDROID_EXPORT_PLUGIN_H +#define ANDROID_EXPORT_PLUGIN_H + #include "godot_plugin_config.h" #include "core/io/zip_io.h" #include "core/os/os.h" -#include "editor/editor_export.h" +#include "editor/export/editor_export_platform.h" const String SPLASH_CONFIG_XML_CONTENT = R"SPLASH(<?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> @@ -113,7 +116,9 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { static Error copy_gradle_so(void *p_userdata, const SharedObject &p_so); - bool _has_storage_permission(const Vector<String> &p_permissions); + bool _has_read_write_storage_permission(const Vector<String> &p_permissions); + + bool _has_manage_external_storage_permission(const Vector<String> &p_permissions); void _get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions); @@ -210,7 +215,7 @@ public: String get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path); - Error save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, const String &p_path); + Error save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path); void get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags); @@ -228,9 +233,11 @@ public: virtual void get_platform_features(List<String> *r_features) override; - virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override; + virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) override; EditorExportPlatformAndroid(); ~EditorExportPlatformAndroid(); }; + +#endif // ANDROID_EXPORT_PLUGIN_H diff --git a/platform/android/export/godot_plugin_config.cpp b/platform/android/export/godot_plugin_config.cpp index 1210c09666..3daf6e44cd 100644 --- a/platform/android/export/godot_plugin_config.cpp +++ b/platform/android/export/godot_plugin_config.cpp @@ -71,7 +71,6 @@ PluginConfigAndroid PluginConfigAndroid::resolve_prebuilt_plugin(PluginConfigAnd Vector<PluginConfigAndroid> PluginConfigAndroid::get_prebuilt_plugins(String plugins_base_dir) { Vector<PluginConfigAndroid> prebuilt_plugins; - // prebuilt_plugins.push_back(resolve_prebuilt_plugin(MY_PREBUILT_PLUGIN, plugins_base_dir)); return prebuilt_plugins; } diff --git a/platform/android/export/godot_plugin_config.h b/platform/android/export/godot_plugin_config.h index 51cb4dea47..5188f615d4 100644 --- a/platform/android/export/godot_plugin_config.h +++ b/platform/android/export/godot_plugin_config.h @@ -103,4 +103,4 @@ struct PluginConfigAndroid { static String get_plugins_names(Vector<PluginConfigAndroid> plugins_configs); }; -#endif // GODOT_PLUGIN_CONFIG_H +#endif // ANDROID_GODOT_PLUGIN_CONFIG_H diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index 9598d2f9fd..8d370a31a4 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -75,11 +75,10 @@ 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)) { - DirAccess *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 + "'."); - memdelete(filesystem_da); } return OK; } @@ -92,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; } @@ -110,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; } @@ -123,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); @@ -153,14 +150,15 @@ 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."); } return ERR_CANT_OPEN; } da->list_dir_begin(); + Dictionary appnames = ProjectSettings::get_singleton()->get("application/config/name_localized"); while (true) { String file = da->get_next(); if (file.is_empty()) { @@ -171,10 +169,9 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset continue; } String locale = file.replace("values-", "").replace("-r", "_"); - String property_name = "application/config/name_" + locale; String locale_directory = "res://android/build/res/" + file + "/godot_project_name_string.xml"; - if (ProjectSettings::get_singleton()->has_setting(property_name)) { - String locale_project_name = ProjectSettings::get_singleton()->get(property_name); + if (appnames.has(locale)) { + String locale_project_name = appnames[locale]; String processed_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(locale_project_name)); print_verbose("Storing project name for locale " + locale + " under " + locale_directory); store_string_at_path(locale_directory, processed_xml_string); @@ -235,30 +232,19 @@ String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset) { return manifest_xr_features; } -String _get_instrumentation_tag(const Ref<EditorExportPreset> &p_preset) { - String package_name = p_preset->get("package/unique_name"); - String manifest_instrumentation_text = vformat( - " <instrumentation\n" - " tools:node=\"replace\"\n" - " android:name=\".GodotInstrumentation\"\n" - " android:icon=\"@mipmap/icon\"\n" - " android:label=\"@string/godot_project_name_string\"\n" - " android:targetPackage=\"%s\" />\n", - package_name); - return manifest_instrumentation_text; -} - String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) { int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); bool uses_xr = xr_mode_index == XR_MODE_OPENXR; 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 { @@ -268,7 +254,7 @@ String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) { return manifest_activity_text; } -String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_storage_permission) { +String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission) { int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); bool uses_xr = xr_mode_index == XR_MODE_OPENXR; String manifest_application_text = vformat( @@ -280,11 +266,12 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ " android:requestLegacyExternalStorage=\"%s\"\n" " tools:replace=\"android:allowBackup,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n" " tools:ignore=\"GoogleAppIndexingWarning\">\n\n" + " <meta-data tools:node=\"remove\" android:name=\"xr_hand_tracking_version_name\" />\n" " <meta-data tools:node=\"remove\" android:name=\"xr_hand_tracking_metadata_name\" />\n", bool_to_string(p_preset->get("user_data_backup/allow")), bool_to_string(p_preset->get("package/classify_as_game")), bool_to_string(p_preset->get("package/retain_data_on_uninstall")), - bool_to_string(p_has_storage_permission)); + bool_to_string(p_has_read_write_storage_permission)); if (uses_xr) { bool hand_tracking_enabled = (int)(p_preset->get("xr_features/hand_tracking")) > XR_HAND_TRACKING_NONE; @@ -294,6 +281,7 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ manifest_application_text += vformat( " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.frequency\" android:value=\"%s\" />\n", hand_tracking_frequency); + manifest_application_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.version\" android:value=\"V2.0\" />\n"; } } else { manifest_application_text += " <meta-data tools:node=\"remove\" android:name=\"com.oculus.supportedDevices\" />\n"; diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 30a7f04729..232b4458c6 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -28,14 +28,14 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef GODOT_GRADLE_EXPORT_UTIL_H -#define GODOT_GRADLE_EXPORT_UTIL_H +#ifndef ANDROID_GRADLE_EXPORT_UTIL_H +#define ANDROID_GRADLE_EXPORT_UTIL_H #include "core/io/dir_access.h" #include "core/io/file_access.h" #include "core/io/zip_io.h" #include "core/os/os.h" -#include "editor/editor_export.h" +#include "editor/export/editor_export.h" const String godot_project_name_xml_string = R"(<?xml version="1.0" encoding="utf-8"?> <!--WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME--> @@ -102,10 +102,8 @@ String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset); String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset); -String _get_instrumentation_tag(const Ref<EditorExportPreset> &p_preset); - String _get_activity_tag(const Ref<EditorExportPreset> &p_preset); -String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_storage_permission); +String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission); -#endif //GODOT_GRADLE_EXPORT_UTIL_H +#endif // ANDROID_GRADLE_EXPORT_UTIL_H diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp index c84a919b6b..ace7636e6c 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -34,12 +34,20 @@ AAssetManager *FileAccessAndroid::asset_manager = nullptr; -FileAccess *FileAccessAndroid::create_android() { - return memnew(FileAccessAndroid); +String FileAccessAndroid::get_path() const { + return path_src; +} + +String FileAccessAndroid::get_path_absolute() const { + return absolute_path; } Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) { + _close(); + + path_src = p_path; String path = fix_path(p_path).simplify_path(); + absolute_path = path; if (path.begins_with("/")) { path = path.substr(1, path.length()); } else if (path.begins_with("res://")) { @@ -47,33 +55,33 @@ Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) { } ERR_FAIL_COND_V(p_mode_flags & FileAccess::WRITE, ERR_UNAVAILABLE); //can't write on android.. - a = AAssetManager_open(asset_manager, path.utf8().get_data(), AASSET_MODE_STREAMING); - if (!a) { + asset = AAssetManager_open(asset_manager, path.utf8().get_data(), AASSET_MODE_STREAMING); + if (!asset) { return ERR_CANT_OPEN; } - len = AAsset_getLength(a); + len = AAsset_getLength(asset); pos = 0; eof = false; return OK; } -void FileAccessAndroid::close() { - if (!a) { +void FileAccessAndroid::_close() { + if (!asset) { return; } - AAsset_close(a); - a = nullptr; + AAsset_close(asset); + asset = nullptr; } bool FileAccessAndroid::is_open() const { - return a != nullptr; + return asset != nullptr; } void FileAccessAndroid::seek(uint64_t p_position) { - ERR_FAIL_COND(!a); + ERR_FAIL_NULL(asset); - AAsset_seek(a, p_position, SEEK_SET); + AAsset_seek(asset, p_position, SEEK_SET); pos = p_position; if (pos > len) { pos = len; @@ -84,8 +92,8 @@ void FileAccessAndroid::seek(uint64_t p_position) { } void FileAccessAndroid::seek_end(int64_t p_position) { - ERR_FAIL_COND(!a); - AAsset_seek(a, p_position, SEEK_END); + ERR_FAIL_NULL(asset); + AAsset_seek(asset, p_position, SEEK_END); pos = len + p_position; } @@ -108,7 +116,7 @@ uint8_t FileAccessAndroid::get_8() const { } uint8_t byte; - AAsset_read(a, &byte, 1); + AAsset_read(asset, &byte, 1); pos++; return byte; } @@ -116,7 +124,7 @@ uint8_t FileAccessAndroid::get_8() const { uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const { ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); - int r = AAsset_read(a, p_dst, p_length); + int r = AAsset_read(asset, p_dst, p_length); if (pos + p_length > len) { eof = true; @@ -132,7 +140,7 @@ uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const } Error FileAccessAndroid::get_error() const { - return eof ? ERR_FILE_EOF : OK; //not sure what else it may happen + return eof ? ERR_FILE_EOF : OK; // not sure what else it may happen } void FileAccessAndroid::flush() { @@ -162,5 +170,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..e6fd8c857b 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -35,45 +35,49 @@ #include <android/asset_manager.h> #include <android/log.h> #include <stdio.h> -//#include <android_native_app_glue.h> class FileAccessAndroid : public FileAccess { - static FileAccess *create_android(); - mutable AAsset *a = nullptr; + mutable AAsset *asset = nullptr; mutable uint64_t len = 0; mutable uint64_t pos = 0; mutable bool eof = false; + String absolute_path; + String path_src; + + 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 Error _open(const String &p_path, int p_mode_flags); // open a file + virtual bool is_open() const; // true when file is open + + /// returns the path for the current open file + virtual String get_path() const; + /// returns the absolute path for the current open file + virtual String get_path_absolute() const; - virtual void seek(uint64_t p_position); ///< seek to a given position - virtual void seek_end(int64_t p_position = 0); ///< seek from the end of file - virtual uint64_t get_position() const; ///< get position in the file - virtual uint64_t get_length() const; ///< get size of the file + virtual void seek(uint64_t p_position); // seek to a given position + virtual void seek_end(int64_t p_position = 0); // seek from the end of file + virtual uint64_t get_position() const; // get position in the file + virtual uint64_t get_length() const; // get size of the file - virtual bool eof_reached() const; ///< reading passed EOF + virtual bool eof_reached() const; // reading passed EOF - virtual uint8_t get_8() const; ///< get a byte + virtual uint8_t get_8() const; // get a byte virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const; - virtual Error get_error() const; ///< get last error + virtual Error get_error() const; // get last error virtual void flush(); - virtual void store_8(uint8_t p_dest); ///< store a byte + virtual void store_8(uint8_t p_dest); // store a byte - virtual bool file_exists(const String &p_path); ///< return true if a file exists + virtual bool file_exists(const String &p_path); // return true if a file exists virtual uint64_t _get_modified_time(const String &p_file) { return 0; } virtual uint32_t _get_unix_permissions(const String &p_file) { return 0; } virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) { return FAILED; } - //static void make_default(); - ~FileAccessAndroid(); }; diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp new file mode 100644 index 0000000000..733d92f741 --- /dev/null +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -0,0 +1,329 @@ +/*************************************************************************/ +/* file_access_filesystem_jandroid.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 "file_access_filesystem_jandroid.h" +#include "core/os/os.h" +#include "core/templates/local_vector.h" +#include "thread_jandroid.h" +#include <unistd.h> + +jobject FileAccessFilesystemJAndroid::file_access_handler = nullptr; +jclass FileAccessFilesystemJAndroid::cls; + +jmethodID FileAccessFilesystemJAndroid::_file_open = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_get_size = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_seek = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_seek_end = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_read = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_tell = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_eof = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_close = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_write = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_flush = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_exists = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_last_modified = nullptr; + +String FileAccessFilesystemJAndroid::get_path() const { + return path_src; +} + +String FileAccessFilesystemJAndroid::get_path_absolute() const { + return absolute_path; +} + +Error FileAccessFilesystemJAndroid::_open(const String &p_path, int p_mode_flags) { + if (is_open()) { + _close(); + } + + if (_file_open) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); + + String path = fix_path(p_path).simplify_path(); + jstring js = env->NewStringUTF(path.utf8().get_data()); + int res = env->CallIntMethod(file_access_handler, _file_open, js, p_mode_flags); + env->DeleteLocalRef(js); + + if (res <= 0) { + switch (res) { + case 0: + default: + return ERR_FILE_CANT_OPEN; + + case -1: + return ERR_FILE_NOT_FOUND; + } + } + + id = res; + path_src = p_path; + absolute_path = path; + return OK; + } else { + return ERR_UNCONFIGURED; + } +} + +void FileAccessFilesystemJAndroid::_close() { + if (!is_open()) { + return; + } + + if (_file_close) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(file_access_handler, _file_close, id); + } + id = 0; +} + +bool FileAccessFilesystemJAndroid::is_open() const { + return id != 0; +} + +void FileAccessFilesystemJAndroid::seek(uint64_t p_position) { + if (_file_seek) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); + env->CallVoidMethod(file_access_handler, _file_seek, id, p_position); + } +} + +void FileAccessFilesystemJAndroid::seek_end(int64_t p_position) { + if (_file_seek_end) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); + env->CallVoidMethod(file_access_handler, _file_seek_end, id, p_position); + } +} + +uint64_t FileAccessFilesystemJAndroid::get_position() const { + if (_file_tell) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + return env->CallLongMethod(file_access_handler, _file_tell, id); + } else { + return 0; + } +} + +uint64_t FileAccessFilesystemJAndroid::get_length() const { + if (_file_get_size) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + return env->CallLongMethod(file_access_handler, _file_get_size, id); + } else { + return 0; + } +} + +bool FileAccessFilesystemJAndroid::eof_reached() const { + if (_file_eof) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_COND_V_MSG(!is_open(), false, "File must be opened before use."); + return env->CallBooleanMethod(file_access_handler, _file_eof, id); + } else { + return false; + } +} + +uint8_t FileAccessFilesystemJAndroid::get_8() const { + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + uint8_t byte; + get_buffer(&byte, 1); + return byte; +} + +String FileAccessFilesystemJAndroid::get_line() const { + ERR_FAIL_COND_V_MSG(!is_open(), String(), "File must be opened before use."); + + const size_t buffer_size_limit = 2048; + const uint64_t file_size = get_length(); + const uint64_t start_position = get_position(); + + String result; + LocalVector<uint8_t> line_buffer; + size_t current_buffer_size = 0; + uint64_t line_buffer_position = 0; + + while (true) { + size_t line_buffer_size = MIN(buffer_size_limit, file_size - get_position()); + if (line_buffer_size <= 0) { + break; + } + + current_buffer_size += line_buffer_size; + line_buffer.resize(current_buffer_size); + + uint64_t bytes_read = get_buffer(&line_buffer[line_buffer_position], current_buffer_size - line_buffer_position); + if (bytes_read <= 0) { + break; + } + + for (; bytes_read > 0; line_buffer_position++, bytes_read--) { + uint8_t elem = line_buffer[line_buffer_position]; + if (elem == '\n' || elem == '\0') { + // Found the end of the line + const_cast<FileAccessFilesystemJAndroid *>(this)->seek(start_position + line_buffer_position + 1); + if (result.parse_utf8((const char *)line_buffer.ptr(), line_buffer_position)) { + return String(); + } + return result; + } + } + } + + if (result.parse_utf8((const char *)line_buffer.ptr(), line_buffer_position)) { + return String(); + } + return result; +} + +uint64_t FileAccessFilesystemJAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const { + if (_file_read) { + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + if (p_length == 0) { + return 0; + } + + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + + jobject j_buffer = env->NewDirectByteBuffer(p_dst, p_length); + int length = env->CallIntMethod(file_access_handler, _file_read, id, j_buffer); + env->DeleteLocalRef(j_buffer); + return length; + } else { + return 0; + } +} + +void FileAccessFilesystemJAndroid::store_8(uint8_t p_dest) { + store_buffer(&p_dest, 1); +} + +void FileAccessFilesystemJAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) { + if (_file_write) { + ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); + if (p_length == 0) { + return; + } + + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + + jobject j_buffer = env->NewDirectByteBuffer((void *)p_src, p_length); + env->CallVoidMethod(file_access_handler, _file_write, id, j_buffer); + env->DeleteLocalRef(j_buffer); + } +} + +Error FileAccessFilesystemJAndroid::get_error() const { + if (eof_reached()) { + return ERR_FILE_EOF; + } + return OK; +} + +void FileAccessFilesystemJAndroid::flush() { + if (_file_flush) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); + env->CallVoidMethod(file_access_handler, _file_flush, id); + } +} + +bool FileAccessFilesystemJAndroid::file_exists(const String &p_path) { + if (_file_exists) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + + String path = fix_path(p_path).simplify_path(); + jstring js = env->NewStringUTF(path.utf8().get_data()); + bool result = env->CallBooleanMethod(file_access_handler, _file_exists, js); + env->DeleteLocalRef(js); + return result; + } else { + return false; + } +} + +uint64_t FileAccessFilesystemJAndroid::_get_modified_time(const String &p_file) { + if (_file_last_modified) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + + String path = fix_path(p_file).simplify_path(); + jstring js = env->NewStringUTF(path.utf8().get_data()); + uint64_t result = env->CallLongMethod(file_access_handler, _file_last_modified, js); + env->DeleteLocalRef(js); + return result; + } else { + return 0; + } +} + +void FileAccessFilesystemJAndroid::setup(jobject p_file_access_handler) { + JNIEnv *env = get_jni_env(); + file_access_handler = env->NewGlobalRef(p_file_access_handler); + + jclass c = env->GetObjectClass(file_access_handler); + cls = (jclass)env->NewGlobalRef(c); + + _file_open = env->GetMethodID(cls, "fileOpen", "(Ljava/lang/String;I)I"); + _file_get_size = env->GetMethodID(cls, "fileGetSize", "(I)J"); + _file_tell = env->GetMethodID(cls, "fileGetPosition", "(I)J"); + _file_eof = env->GetMethodID(cls, "isFileEof", "(I)Z"); + _file_seek = env->GetMethodID(cls, "fileSeek", "(IJ)V"); + _file_seek_end = env->GetMethodID(cls, "fileSeekFromEnd", "(IJ)V"); + _file_read = env->GetMethodID(cls, "fileRead", "(ILjava/nio/ByteBuffer;)I"); + _file_close = env->GetMethodID(cls, "fileClose", "(I)V"); + _file_write = env->GetMethodID(cls, "fileWrite", "(ILjava/nio/ByteBuffer;)V"); + _file_flush = env->GetMethodID(cls, "fileFlush", "(I)V"); + _file_exists = env->GetMethodID(cls, "fileExists", "(Ljava/lang/String;)Z"); + _file_last_modified = env->GetMethodID(cls, "fileLastModified", "(Ljava/lang/String;)J"); +} + +FileAccessFilesystemJAndroid::FileAccessFilesystemJAndroid() { + id = 0; +} + +FileAccessFilesystemJAndroid::~FileAccessFilesystemJAndroid() { + if (is_open()) { + _close(); + } +} diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h new file mode 100644 index 0000000000..7deb8de37b --- /dev/null +++ b/platform/android/file_access_filesystem_jandroid.h @@ -0,0 +1,98 @@ +/*************************************************************************/ +/* file_access_filesystem_jandroid.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 FILE_ACCESS_FILESYSTEM_JANDROID_H +#define FILE_ACCESS_FILESYSTEM_JANDROID_H + +#include "core/io/file_access.h" +#include "java_godot_lib_jni.h" + +class FileAccessFilesystemJAndroid : public FileAccess { + static jobject file_access_handler; + static jclass cls; + + static jmethodID _file_open; + static jmethodID _file_get_size; + static jmethodID _file_seek; + static jmethodID _file_seek_end; + static jmethodID _file_tell; + static jmethodID _file_eof; + static jmethodID _file_read; + static jmethodID _file_write; + static jmethodID _file_flush; + static jmethodID _file_close; + static jmethodID _file_exists; + static jmethodID _file_last_modified; + + int id; + String absolute_path; + String path_src; + + void _close(); ///< close a file + +public: + virtual Error _open(const String &p_path, int p_mode_flags) override; ///< open a file + virtual bool is_open() const override; ///< true when file is open + + /// returns the path for the current open file + virtual String get_path() const override; + /// returns the absolute path for the current open file + virtual String get_path_absolute() const override; + + virtual void seek(uint64_t p_position) override; ///< seek to a given position + virtual void seek_end(int64_t p_position = 0) override; ///< seek from the end of file + virtual uint64_t get_position() const override; ///< get position in the file + virtual uint64_t get_length() const override; ///< get size of the file + + virtual bool eof_reached() const override; ///< reading passed EOF + + virtual uint8_t get_8() const override; ///< get a byte + virtual String get_line() const override; ///< get a line + virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; + + virtual Error get_error() const override; ///< get last error + + virtual void flush() override; + virtual void store_8(uint8_t p_dest) override; ///< store a byte + virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; + + virtual bool file_exists(const String &p_path) override; ///< return true if a file exists + + static void setup(jobject p_file_access_handler); + + virtual uint64_t _get_modified_time(const String &p_file) override; + virtual uint32_t _get_unix_permissions(const String &p_file) override { return 0; } + virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) override { return FAILED; } + + FileAccessFilesystemJAndroid(); + ~FileAccessFilesystemJAndroid(); +}; + +#endif // FILE_ACCESS_FILESYSTEM_JANDROID_H diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 4c4501729d..2d4c4763a2 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -40,6 +40,13 @@ android:name="xr_hand_tracking_metadata_name" android:value="xr_hand_tracking_metadata_value"/> + <!-- XR hand tracking version --> + <!-- This is modified by the exporter based on the selected xr mode. DO NOT CHANGE the values here. --> + <!-- Removed at export time if the xr mode is not VR or hand tracking is disabled. --> + <meta-data + android:name="xr_hand_tracking_version_name" + android:value="xr_hand_tracking_version_value"/> + <!-- Supported Meta devices --> <!-- This is removed by the exporter if the xr mode is not VR. --> <meta-data @@ -52,6 +59,7 @@ android:theme="@style/GodotAppSplashTheme" android:launchMode="singleTask" android:excludeFromRecents="false" + android:exported="true" android:screenOrientation="landscape" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:resizeableActivity="false" diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 5d1a9d7b99..63b10e62b1 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -1,6 +1,4 @@ // Gradle build config for Godot Engine's Android port. -apply from: 'config.gradle' - buildscript { apply from: 'config.gradle' @@ -14,7 +12,12 @@ buildscript { } } -apply plugin: 'com.android.application' +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +apply from: 'config.gradle' allprojects { repositories { @@ -33,6 +36,11 @@ allprojects { } } +configurations { + // Initializes a placeholder for the devImplementation dependency configuration. + devImplementation {} +} + dependencies { implementation libraries.kotlinStdLib implementation libraries.androidxFragment @@ -45,6 +53,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,12 +75,17 @@ dependencies { android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion compileOptions { sourceCompatibility versions.javaVersion targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + assetPacks = [":assetPacks:installTime"] defaultConfig { @@ -93,6 +107,8 @@ android { versionName getExportVersionName() minSdkVersion getExportMinSdkVersion() targetSdkVersion getExportTargetSdkVersion() + + missingDimensionStrategy 'products', 'template' } lintOptions { @@ -146,6 +162,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 +195,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 +212,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 +230,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 32e03998da..fbd97fae0b 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -1,20 +1,21 @@ ext.versions = [ androidGradlePlugin: '7.0.3', - compileSdk : 30, + compileSdk : 32, minSdk : 19, // Also update 'platform/android/java/lib/AndroidManifest.xml#minSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION' - targetSdk : 30, // Also update 'platform/android/java/lib/AndroidManifest.xml#targetSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION' - buildTools : '30.0.3', - kotlinVersion : '1.5.10', + targetSdk : 32, // Also update 'platform/android/java/lib/AndroidManifest.xml#targetSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION' + buildTools : '32.0.0', + kotlinVersion : '1.6.21', fragmentVersion : '1.3.6', + nexusPublishVersion: '1.1.0', javaVersion : 11, - ndkVersion : '21.4.7075529' // Also update 'platform/android/detect.py#get_project_ndk_version()' when this is updated. + ndkVersion : '23.2.8568313' // Also update 'platform/android/detect.py#get_ndk_version()' when this is updated. ] ext.libraries = [ androidGradlePlugin: "com.android.tools.build:gradle:$versions.androidGradlePlugin", kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlinVersion", - kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlinVersion", + kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlinVersion", androidxFragment : "androidx.fragment:fragment:$versions.fragmentVersion", ] @@ -76,7 +77,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,13 +87,27 @@ ext.getGodotEditorVersion = { -> return editorVersion } -ext.getGodotLibraryVersion = { -> +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()) { - List<String> requiredKeys = ["major", "minor", "patch", "status", "module_config"] def map = [:] List<String> lines = versionFile.readLines() @@ -110,15 +125,48 @@ ext.getGodotLibraryVersion = { -> } 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 = { -> + List<String> requiredKeys = ["major", "minor", "patch", "status", "module_config"] + return generateGodotLibraryVersion(requiredKeys) +} + +ext.getGodotPublishVersion = { -> + List<String> requiredKeys = ["major", "minor", "patch", "status"] + String versionName = "" + int versionCode = 1 + (versionName, versionCode) = generateGodotLibraryVersion(requiredKeys) + return versionName } final String VALUE_SEPARATOR_REGEX = "\\|" diff --git a/platform/android/java/app/res/values/themes.xml b/platform/android/java/app/res/values/themes.xml index 99f723f5ba..d64b50ca45 100644 --- a/platform/android/java/app/res/values/themes.xml +++ b/platform/android/java/app/res/values/themes.xml @@ -1,9 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen"/> + <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar"/> - <style name="GodotAppSplashTheme" parent="@style/GodotAppMainTheme"> + <style name="GodotAppSplashTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen"> <item name="android:windowBackground">@drawable/splash_drawable</item> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style> diff --git a/platform/android/java/app/settings.gradle b/platform/android/java/app/settings.gradle index e38d7b2ba6..ba53aefe7f 100644 --- a/platform/android/java/app/settings.gradle +++ b/platform/android/java/app/settings.gradle @@ -1,2 +1,15 @@ // This is the root directory of the Godot custom build. +pluginManagement { + apply from: 'config.gradle' + + plugins { + id 'com.android.application' version versions.androidGradlePlugin + id 'org.jetbrains.kotlin.android' version versions.kotlinVersion + } + repositories { + gradlePluginPortal() + google() + } +} + include ':assetPacks:installTime' diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index ac008edbed..da30bd3a95 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -1,18 +1,25 @@ -apply from: 'app/config.gradle' - buildscript { apply from: 'app/config.gradle' repositories { google() mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath libraries.androidGradlePlugin classpath libraries.kotlinGradlePlugin + classpath 'io.github.gradle-nexus:publish-plugin:1.1.0' } } +plugins { + id 'io.github.gradle-nexus.publish-plugin' +} + +apply from: 'app/config.gradle' +apply from: 'scripts/publish-root.gradle' + allprojects { repositories { google() @@ -22,21 +29,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() } /** @@ -51,6 +59,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. */ @@ -66,7 +85,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') @@ -77,18 +96,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') @@ -99,7 +140,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') @@ -107,7 +148,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) { @@ -126,8 +167,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 @@ -137,7 +188,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() @@ -163,6 +214,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. */ @@ -187,7 +282,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 @@ -204,9 +319,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..729966ee69 --- /dev/null +++ b/platform/android/java/editor/build.gradle @@ -0,0 +1,82 @@ +// Gradle build config for Godot Engine's Android port. +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +dependencies { + implementation libraries.kotlinStdLib + implementation libraries.androidxFragment + implementation project(":lib") + + implementation "androidx.window:window:1.0.0" +} + +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 + targetSdkVersion versions.targetSdk + + missingDimensionStrategy 'products', 'editor' + } + + compileOptions { + sourceCompatibility versions.javaVersion + targetCompatibility versions.javaVersion + } + + kotlinOptions { + jvmTarget = 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..abf506a83c --- /dev/null +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -0,0 +1,77 @@ +<?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.MANAGE_EXTERNAL_STORAGE" + tools:ignore="ScopedStorage" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" + android:maxSdkVersion="29"/> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" + android:maxSdkVersion="29"/> + <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"> + + <layout android:defaultHeight="@dimen/editor_default_window_height" + android:defaultWidth="@dimen/editor_default_window_width" /> + + <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:exported="false" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"> + <layout android:defaultHeight="@dimen/editor_default_window_height" + android:defaultWidth="@dimen/editor_default_window_width" /> + </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:exported="false" + android:screenOrientation="userLandscape" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"> + <layout android:defaultHeight="@dimen/editor_default_window_height" + android:defaultWidth="@dimen/editor_default_window_width" /> + </activity> + + </application> + +</manifest> diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt new file mode 100644 index 0000000000..740f3f48d3 --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -0,0 +1,196 @@ +/*************************************************************************/ +/* GodotEditor.kt */ +/*************************************************************************/ +/* 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 android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.Debug +import android.os.Environment +import android.widget.Toast +import androidx.window.layout.WindowMetricsCalculator +import org.godotengine.godot.FullScreenGodotApp +import org.godotengine.godot.utils.PermissionsUtil +import java.util.* +import kotlin.math.min + +/** + * 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. + */ +open class GodotEditor : FullScreenGodotApp() { + + companion object { + private const val WAIT_FOR_DEBUGGER = false + + private const val COMMAND_LINE_PARAMS = "command_line_params" + + private const val EDITOR_ARG = "--editor" + private const val PROJECT_MANAGER_ARG = "--project-manager" + } + + private val commandLineParams = ArrayList<String>() + + override fun onCreate(savedInstanceState: Bundle?) { + PermissionsUtil.requestManifestPermissions(this) + + val params = intent.getStringArrayExtra(COMMAND_LINE_PARAMS) + updateCommandLineParams(params) + + if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) { + Debug.waitForDebugger() + } + + super.onCreate(savedInstanceState) + } + + private fun updateCommandLineParams(args: Array<String>?) { + // Update the list of command line params with the new args + commandLineParams.clear() + if (args != null && args.isNotEmpty()) { + commandLineParams.addAll(listOf(*args)) + } + } + + override fun getCommandLine() = commandLineParams + + override fun onNewGodotInstanceRequested(args: Array<String>) { + // Parse the arguments to figure out which activity to start. + var targetClass: Class<*> = GodotGame::class.java + + // Whether we should launch the new godot instance in an adjacent window + // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT + var launchAdjacent = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && (isInMultiWindowMode || isLargeScreen) + + for (arg in args) { + if (EDITOR_ARG == arg) { + targetClass = GodotEditor::class.java + launchAdjacent = false + break + } + + if (PROJECT_MANAGER_ARG == arg) { + targetClass = GodotProjectManager::class.java + launchAdjacent = false + break + } + } + + // Launch a new activity + val newInstance = Intent(this, targetClass) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(COMMAND_LINE_PARAMS, args) + if (launchAdjacent) { + newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) + } + startActivity(newInstance) + } + + // Get the screen's density scale + protected val isLargeScreen: Boolean + // Get the minimum window size // Correspond to the EXPANDED window size class. + get() { + val metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this) + + // Get the screen's density scale + val scale = resources.displayMetrics.density + + // Get the minimum window size + val minSize = min(metrics.bounds.width(), metrics.bounds.height()).toFloat() + val minSizeDp = minSize / scale + return minSizeDp >= 840f // Correspond to the EXPANDED window size class. + } + + override fun setRequestedOrientation(requestedOrientation: Int) { + if (!overrideOrientationRequest()) { + super.setRequestedOrientation(requestedOrientation) + } + } + + /** + * The Godot Android Editor sets its own orientation via its AndroidManifest + */ + protected open fun overrideOrientationRequest() = true + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + // Check if we got the MANAGE_EXTERNAL_STORAGE permission + if (requestCode == PermissionsUtil.REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (!Environment.isExternalStorageManager()) { + Toast.makeText( + this, + R.string.denied_storage_permission_error_msg, + Toast.LENGTH_LONG + ).show() + } + } + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array<String?>, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + // Check if we got access to the necessary storage permissions + if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + var hasReadAccess = false + var hasWriteAccess = false + for (i in permissions.indices) { + if (Manifest.permission.READ_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) { + hasReadAccess = true + } + if (Manifest.permission.WRITE_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) { + hasWriteAccess = true + } + } + if (!hasReadAccess || !hasWriteAccess) { + Toast.makeText( + this, + R.string.denied_storage_permission_error_msg, + Toast.LENGTH_LONG + ).show() + } + } + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 44f0a3eb3e..783095f93a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -1,5 +1,5 @@ /*************************************************************************/ -/* GodotInstrumentation.java */ +/* GodotGame.kt */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,23 +28,11 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.godot; +package org.godotengine.editor -import android.app.Instrumentation; -import android.content.Intent; -import android.os.Bundle; - -public class GodotInstrumentation extends Instrumentation { - private Intent intent; - - @Override - public void onCreate(Bundle arguments) { - intent = arguments.getParcelable("intent"); - start(); - } - - @Override - public void onStart() { - startActivitySync(intent); - } +/** + * Drives the 'run project' window of the Godot Editor. + */ +class GodotGame : GodotEditor() { + override fun overrideOrientationRequest() = false } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt new file mode 100644 index 0000000000..bcf4659603 --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt @@ -0,0 +1,40 @@ +/*************************************************************************/ +/* GodotProjectManager.kt */ +/*************************************************************************/ +/* 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 + * [GodotEditor] activity. + */ +class GodotProjectManager : GodotEditor() diff --git a/platform/android/java/editor/src/main/res/values/dimens.xml b/platform/android/java/editor/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..03fb6184d2 --- /dev/null +++ b/platform/android/java/editor/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="editor_default_window_height">600dp</dimen> + <dimen name="editor_default_window_width">800dp</dimen> +</resources> 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..837a5d62e1 --- /dev/null +++ b/platform/android/java/editor/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_editor_name_string">Godot Editor 4.x</string> + + <string name="denied_storage_permission_error_msg">Missing storage access permission!</string> +</resources> diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml index 2de62271c4..79b5aadf2a 100644 --- a/platform/android/java/lib/AndroidManifest.xml +++ b/platform/android/java/lib/AndroidManifest.xml @@ -5,7 +5,7 @@ android:versionName="1.0"> <!-- Should match the mindSdk and targetSdk values in platform/android/java/app/config.gradle --> - <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="30" /> + <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="32" /> <application> @@ -16,12 +16,13 @@ <service android:name=".GodotDownloaderService" /> - </application> + <activity + android:name=".utils.ProcessPhoenix" + android:theme="@android:style/Theme.Translucent.NoTitleBar" + android:process=":phoenix" + android:exported="false" + /> - <instrumentation - android:icon="@mipmap/icon" - android:label="@string/godot_project_name_string" - android:name="org.godotengine.godot.GodotInstrumentation" - android:targetPackage="org.godotengine.godot" /> + </application> </manifest> diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index fbed4ed078..6b82326a27 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -1,5 +1,14 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +ext { + PUBLISH_VERSION = getGodotPublishVersion() + PUBLISH_ARTIFACT_ID = 'godot' +} + +apply from: "../scripts/publish-module.gradle" dependencies { implementation libraries.kotlinStdLib @@ -11,21 +20,38 @@ 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" + compileOptions { sourceCompatibility versions.javaVersion targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + + buildTypes { + dev { + initWith debug + } + } + + flavorDimensions "products" + productFlavors { + editor {} + template {} + } + lintOptions { abortOnError false disable 'MissingTranslation', 'UnusedResources' @@ -49,24 +75,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 buildType = variant.buildType.name + if (buildType == null || buildType == "" || !supportedTargetsMap.containsKey(buildType)) { + throw new GradleException("Invalid build type: $buildType") + } - def releaseTarget = buildType.toLowerCase() - if (releaseTarget == null || releaseTarget == "") { - throw new GradleException("Invalid build type: " + buildType) + def sconsTarget = supportedTargetsMap[buildType] + if (sconsTarget == null || sconsTarget == "") { + throw new GradleException("Invalid scons target: $sconsTarget") } - if (!supportedAbis.contains(defaultAbi)) { - throw new GradleException("Invalid default abi: " + defaultAbi) + // 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 @@ -79,13 +131,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()) { @@ -94,21 +144,34 @@ 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("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..f21f88db0a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -30,7 +30,8 @@ package org.godotengine.godot; -import android.content.ComponentName; +import org.godotengine.godot.utils.ProcessPhoenix; + import android.content.Intent; import android.os.Bundle; import android.util.Log; @@ -65,16 +66,13 @@ 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(); } } @Override public void onDestroy() { + Log.v(TAG, "Destroying Godot app..."); super.onDestroy(); onGodotForceQuit(godotFragment); } @@ -82,27 +80,21 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God @Override public final void onGodotForceQuit(Godot instance) { if (instance == godotFragment) { - System.exit(0); + Log.v(TAG, "Force quitting Godot instance"); + ProcessPhoenix.forceQuit(this); } } @Override public final void onGodotRestartRequested(Godot instance) { if (instance == godotFragment) { - // HACK: - // - // Currently it's very hard to properly deinitialize Godot on Android to restart the game + // It's very hard to properly de-initialize Godot on Android to restart the game // from scratch. Therefore, we need to kill the whole app process and relaunch it. // // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including // releasing and reloading native libs or resetting their state somehow and clearing statics). - // - // Using instrumentation is a way of making the whole app process restart, because Android - // will kill any process of the same package which was already running. - // - Bundle args = new Bundle(); - args.putParcelable("intent", getIntent()); - startInstrumentation(new ComponentName(this, GodotInstrumentation.class), null, args); + Log.v(TAG, "Restarting Godot instance..."); + ProcessPhoenix.triggerRebirth(this); } } 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..28e689e63a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -34,8 +34,11 @@ import static android.content.Context.MODE_PRIVATE; import static android.content.Context.WINDOW_SERVICE; import org.godotengine.godot.input.GodotEditText; +import org.godotengine.godot.io.directory.DirectoryAccessHandler; +import org.godotengine.godot.io.file.FileAccessHandler; import org.godotengine.godot.plugin.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 +50,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; @@ -164,8 +166,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private Sensor mMagnetometer; private Sensor mGyroscope; - public static GodotIO io; - public static GodotNetUtils netUtils; + public GodotIO io; + public GodotNetUtils netUtils; + public GodotTTS tts; public interface ResultCallback { void callback(int requestCode, int resultCode, Intent data); @@ -333,9 +336,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) { @@ -455,19 +460,26 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final Activity activity = getActivity(); io = new GodotIO(activity); - GodotLib.io = io; netUtils = new GodotNetUtils(activity); + tts = new GodotTTS(activity); + Context context = getContext(); + DirectoryAccessHandler directoryAccessHandler = new DirectoryAccessHandler(context); + FileAccessHandler fileAccessHandler = new FileAccessHandler(context); mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); - mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME); mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); - mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); - mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); - GodotLib.initialize(activity, this, activity.getAssets(), use_apk_expansion); + GodotLib.initialize(activity, + this, + activity.getAssets(), + io, + netUtils, + directoryAccessHandler, + fileAccessHandler, + use_apk_expansion, + tts); result_callback = null; @@ -508,17 +520,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 +674,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 +706,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 +726,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 +865,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 +893,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 +1017,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal)); } + public void initInputDevices() { mRenderView.initInputDevices(); } @@ -1018,4 +1026,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..0434efdf4c 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -36,8 +36,8 @@ import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.content.res.AssetManager; import android.graphics.Point; +import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Environment; @@ -45,12 +45,11 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; -import android.util.SparseArray; import android.view.Display; import android.view.DisplayCutout; import android.view.WindowInsets; -import java.io.IOException; +import java.util.List; import java.util.Locale; // Wrapper for native library @@ -58,7 +57,6 @@ import java.util.Locale; public class GodotIO { private static final String TAG = GodotIO.class.getSimpleName(); - private final AssetManager am; private final Activity activity; private final String uniqueId; GodotEditText edit; @@ -71,100 +69,8 @@ public class GodotIO { final int SCREEN_SENSOR_PORTRAIT = 5; final int SCREEN_SENSOR = 6; - ///////////////////////// - /// DIRECTORIES - ///////////////////////// - - static class AssetDir { - public String[] files; - public int current; - public String path; - } - - private int last_dir_id = 1; - - private final SparseArray<AssetDir> dirs; - - public int dir_open(String path) { - AssetDir ad = new AssetDir(); - ad.current = 0; - ad.path = path; - - try { - ad.files = am.list(path); - // no way to find path is directory or file exactly. - // but if ad.files.length==0, then it's an empty directory or file. - if (ad.files.length == 0) { - return -1; - } - } catch (IOException e) { - System.out.printf("Exception on dir_open: %s\n", e); - return -1; - } - - ++last_dir_id; - dirs.put(last_dir_id, ad); - - return last_dir_id; - } - - public boolean dir_is_dir(int id) { - if (dirs.get(id) == null) { - System.out.printf("dir_next: invalid dir id: %d\n", id); - return false; - } - AssetDir ad = dirs.get(id); - //System.out.printf("go next: %d,%d\n",ad.current,ad.files.length); - int idx = ad.current; - if (idx > 0) - idx--; - - if (idx >= ad.files.length) - return false; - String fname = ad.files[idx]; - - try { - if (ad.path.equals("")) - am.open(fname); - else - am.open(ad.path + "/" + fname); - return false; - } catch (Exception e) { - return true; - } - } - - public String dir_next(int id) { - if (dirs.get(id) == null) { - System.out.printf("dir_next: invalid dir id: %d\n", id); - return ""; - } - - AssetDir ad = dirs.get(id); - //System.out.printf("go next: %d,%d\n",ad.current,ad.files.length); - - if (ad.current >= ad.files.length) { - ad.current++; - return ""; - } - String r = ad.files[ad.current]; - ad.current++; - return r; - } - - public void dir_close(int id) { - if (dirs.get(id) == null) { - System.out.printf("dir_close: invalid dir id: %d\n", id); - return; - } - - dirs.remove(id); - } - GodotIO(Activity p_activity) { - am = p_activity.getAssets(); activity = p_activity; - dirs = new SparseArray<>(); String androidId = Settings.Secure.getString(activity.getContentResolver(), Settings.Secure.ANDROID_ID); if (androidId == null) { @@ -222,8 +128,30 @@ public class GodotIO { } public int getScreenDPI() { - DisplayMetrics metrics = activity.getApplicationContext().getResources().getDisplayMetrics(); - return (int)(metrics.density * 160f); + return activity.getResources().getDisplayMetrics().densityDpi; + } + + /** + * Returns bucketized density values. + */ + public float getScaledDensity() { + int densityDpi = activity.getResources().getDisplayMetrics().densityDpi; + float selectedScaledDensity; + if (densityDpi >= DisplayMetrics.DENSITY_XXXHIGH) { + selectedScaledDensity = 4.0f; + } else if (densityDpi >= DisplayMetrics.DENSITY_XXHIGH) { + selectedScaledDensity = 3.0f; + } else if (densityDpi >= DisplayMetrics.DENSITY_XHIGH) { + selectedScaledDensity = 2.0f; + } else if (densityDpi >= DisplayMetrics.DENSITY_HIGH) { + selectedScaledDensity = 1.5f; + } else if (densityDpi >= DisplayMetrics.DENSITY_MEDIUM) { + selectedScaledDensity = 1.0f; + } else { + selectedScaledDensity = 0.75f; + } + Log.d(TAG, "Selected scaled density: " + selectedScaledDensity); + return selectedScaledDensity; } public double getScreenRefreshRate(double fallback) { @@ -234,7 +162,7 @@ public class GodotIO { return fallback; } - public int[] screenGetUsableRect() { + public int[] getDisplaySafeArea() { DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); Display display = activity.getWindowManager().getDefaultDisplay(); Point size = new Point(); @@ -256,6 +184,25 @@ public class GodotIO { return result; } + public int[] getDisplayCutouts() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) + return new int[0]; + DisplayCutout cutout = activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout(); + if (cutout == null) + return new int[0]; + List<Rect> rects = cutout.getBoundingRects(); + int cutouts = rects.size(); + int[] result = new int[cutouts * 4]; + int index = 0; + for (Rect rect : rects) { + result[index++] = rect.left; + result[index++] = rect.top; + result[index++] = rect.width(); + result[index++] = rect.height(); + } + return result; + } + public void showKeyboard(String p_existing_text, boolean p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { if (edit != null) edit.showKeyboard(p_existing_text, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end); 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..e2ae62d9cf 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -30,7 +30,14 @@ package org.godotengine.godot; +import org.godotengine.godot.gl.GodotRenderer; +import org.godotengine.godot.io.directory.DirectoryAccessHandler; +import org.godotengine.godot.io.file.FileAccessHandler; +import org.godotengine.godot.tts.GodotTTS; +import org.godotengine.godot.utils.GodotNetUtils; + import android.app.Activity; +import android.content.res.AssetManager; import android.hardware.SensorEvent; import android.view.Surface; @@ -40,8 +47,6 @@ import javax.microedition.khronos.opengles.GL10; * Wrapper for native library */ public class GodotLib { - public static GodotIO io; - static { System.loadLibrary("godot_android"); } @@ -49,7 +54,15 @@ public class GodotLib { /** * Invoked on the main thread to initialize Godot native layer. */ - public static native void initialize(Activity activity, Godot p_instance, Object p_asset_manager, boolean use_apk_expansion); + public static native void initialize(Activity activity, + Godot p_instance, + AssetManager p_asset_manager, + GodotIO godotIO, + GodotNetUtils netUtils, + DirectoryAccessHandler directoryAccessHandler, + FileAccessHandler fileAccessHandler, + boolean use_apk_expansion, + GodotTTS tts); /** * Invoked on the main thread to clean up Godot native layer. @@ -68,7 +81,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 +98,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. @@ -107,11 +125,6 @@ public class GodotLib { public static native void doubleTap(int buttonMask, int x, int y); /** - * Forward scroll events from the main thread to the GL thread. - */ - public static native void scroll(int x, int y); - - /** * Forward accelerometer sensor events from the main thread to the GL thread. * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent) */ 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/GodotGestureHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java index ac13cad23e..778efa914a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java @@ -80,15 +80,6 @@ public class GodotGestureHandler extends GestureDetector.SimpleOnGestureListener } @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - //Log.i("GodotGesture", "onScroll"); - final int x = Math.round(distanceX); - final int y = Math.round(distanceY); - GodotLib.scroll(x, y); - return true; - } - - @Override public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { //Log.i("GodotGesture", "onFling"); return true; 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..ccfb865b1a 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); } @@ -185,6 +186,9 @@ public class GodotInputHandler implements InputDeviceListener { if (mJoystickIds.indexOfKey(deviceId) >= 0) { final int godotJoyId = mJoystickIds.get(deviceId); Joystick joystick = mJoysticksDevices.get(deviceId); + if (joystick == null) { + return true; + } for (int i = 0; i < joystick.axes.size(); i++) { final int axis = joystick.axes.get(i); 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/io/StorageScope.kt b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt new file mode 100644 index 0000000000..c9282dd247 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt @@ -0,0 +1,113 @@ +/*************************************************************************/ +/* StorageScope.kt */ +/*************************************************************************/ +/* 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.io + +import android.content.Context +import android.os.Build +import android.os.Environment +import java.io.File + +/** + * Represents the different storage scopes. + */ +internal enum class StorageScope { + /** + * Covers internal and external directories accessible to the app without restrictions. + */ + APP, + + /** + * Covers shared directories (from Android 10 and higher). + */ + SHARED, + + /** + * Everything else.. + */ + UNKNOWN; + + class Identifier(context: Context) { + + private val internalAppDir: String? = context.filesDir.canonicalPath + private val internalCacheDir: String? = context.cacheDir.canonicalPath + private val externalAppDir: String? = context.getExternalFilesDir(null)?.canonicalPath + private val sharedDir : String? = Environment.getExternalStorageDirectory().canonicalPath + private val downloadsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath + private val documentsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath + + /** + * Determines which [StorageScope] the given path falls under. + */ + fun identifyStorageScope(path: String?): StorageScope { + if (path == null) { + return UNKNOWN + } + + val pathFile = File(path) + if (!pathFile.isAbsolute) { + return UNKNOWN + } + + val canonicalPathFile = pathFile.canonicalPath + + if (internalAppDir != null && canonicalPathFile.startsWith(internalAppDir)) { + return APP + } + + if (internalCacheDir != null && canonicalPathFile.startsWith(internalCacheDir)) { + return APP + } + + if (externalAppDir != null && canonicalPathFile.startsWith(externalAppDir)) { + return APP + } + + if (sharedDir != null && canonicalPathFile.startsWith(sharedDir)) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + // Before R, apps had access to shared storage so long as they have the right + // permissions (and flag on Q). + return APP + } + + // Post R, access is limited based on the target destination + // 'Downloads' and 'Documents' are still accessible + if ((downloadsSharedDir != null && canonicalPathFile.startsWith(downloadsSharedDir)) + || (documentsSharedDir != null && canonicalPathFile.startsWith(documentsSharedDir))) { + return APP + } + + return SHARED + } + + return UNKNOWN + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt new file mode 100644 index 0000000000..098b10ae36 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt @@ -0,0 +1,177 @@ +/*************************************************************************/ +/* AssetsDirectoryAccess.kt */ +/*************************************************************************/ +/* 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.io.directory + +import android.content.Context +import android.util.Log +import android.util.SparseArray +import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID +import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID +import java.io.File +import java.io.IOException + +/** + * Handles directories access within the Android assets directory. + */ +internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.DirectoryAccess { + + companion object { + private val TAG = AssetsDirectoryAccess::class.java.simpleName + } + + private data class AssetDir(val path: String, val files: Array<String>, var current: Int = 0) + + private val assetManager = context.assets + + private var lastDirId = STARTING_DIR_ID + private val dirs = SparseArray<AssetDir>() + + private fun getAssetsPath(originalPath: String): String { + if (originalPath.startsWith(File.separatorChar)) { + return originalPath.substring(1) + } + return originalPath + } + + override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0 + + override fun dirOpen(path: String): Int { + val assetsPath = getAssetsPath(path) ?: return INVALID_DIR_ID + try { + val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID + // Empty directories don't get added to the 'assets' directory, so + // if ad.files.length > 0 ==> path is directory + // if ad.files.length == 0 ==> path is file + if (files.isEmpty()) { + return INVALID_DIR_ID + } + + val ad = AssetDir(assetsPath, files) + + dirs.put(++lastDirId, ad) + return lastDirId + } catch (e: IOException) { + Log.e(TAG, "Exception on dirOpen", e) + return INVALID_DIR_ID + } + } + + override fun dirExists(path: String): Boolean { + val assetsPath = getAssetsPath(path) + try { + val files = assetManager.list(assetsPath) ?: return false + // Empty directories don't get added to the 'assets' directory, so + // if ad.files.length > 0 ==> path is directory + // if ad.files.length == 0 ==> path is file + return files.isNotEmpty() + } catch (e: IOException) { + Log.e(TAG, "Exception on dirExists", e) + return false + } + } + + override fun fileExists(path: String): Boolean { + val assetsPath = getAssetsPath(path) ?: return false + try { + val files = assetManager.list(assetsPath) ?: return false + // Empty directories don't get added to the 'assets' directory, so + // if ad.files.length > 0 ==> path is directory + // if ad.files.length == 0 ==> path is file + return files.isEmpty() + } catch (e: IOException) { + Log.e(TAG, "Exception on fileExists", e) + return false + } + } + + override fun dirIsDir(dirId: Int): Boolean { + val ad: AssetDir = dirs[dirId] + + var idx = ad.current + if (idx > 0) { + idx-- + } + + if (idx >= ad.files.size) { + return false + } + + val fileName = ad.files[idx] + // List the contents of $fileName. If it's a file, it will be empty, otherwise it'll be a + // directory + val filePath = if (ad.path == "") fileName else "${ad.path}/${fileName}" + val fileContents = assetManager.list(filePath) + return (fileContents?.size?: 0) > 0 + } + + override fun isCurrentHidden(dirId: Int): Boolean { + val ad = dirs[dirId] + + var idx = ad.current + if (idx > 0) { + idx-- + } + + if (idx >= ad.files.size) { + return false + } + + val fileName = ad.files[idx] + return fileName.startsWith('.') + } + + override fun dirNext(dirId: Int): String { + val ad: AssetDir = dirs[dirId] + + if (ad.current >= ad.files.size) { + ad.current++ + return "" + } + + return ad.files[ad.current++] + } + + override fun dirClose(dirId: Int) { + dirs.remove(dirId) + } + + override fun getDriveCount() = 0 + + override fun getDrive(drive: Int) = "" + + override fun makeDir(dir: String) = false + + override fun getSpaceLeft() = 0L + + override fun rename(from: String, to: String) = false + + override fun remove(filename: String) = false +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt new file mode 100644 index 0000000000..fedcf4843f --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt @@ -0,0 +1,224 @@ +/*************************************************************************/ +/* DirectoryAccessHandler.kt */ +/*************************************************************************/ +/* 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.io.directory + +import android.content.Context +import android.util.Log +import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM +import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES + +/** + * Handles files and directories access and manipulation for the Android platform + */ +class DirectoryAccessHandler(context: Context) { + + companion object { + private val TAG = DirectoryAccessHandler::class.java.simpleName + + internal const val INVALID_DIR_ID = -1 + internal const val STARTING_DIR_ID = 1 + + private fun getAccessTypeFromNative(accessType: Int): AccessType? { + return when (accessType) { + ACCESS_RESOURCES.nativeValue -> ACCESS_RESOURCES + ACCESS_FILESYSTEM.nativeValue -> ACCESS_FILESYSTEM + else -> null + } + } + } + + private enum class AccessType(val nativeValue: Int) { + ACCESS_RESOURCES(0), ACCESS_FILESYSTEM(2) + } + + internal interface DirectoryAccess { + fun dirOpen(path: String): Int + fun dirNext(dirId: Int): String + fun dirClose(dirId: Int) + fun dirIsDir(dirId: Int): Boolean + fun dirExists(path: String): Boolean + fun fileExists(path: String): Boolean + fun hasDirId(dirId: Int): Boolean + fun isCurrentHidden(dirId: Int): Boolean + fun getDriveCount() : Int + fun getDrive(drive: Int): String + fun makeDir(dir: String): Boolean + fun getSpaceLeft(): Long + fun rename(from: String, to: String): Boolean + fun remove(filename: String): Boolean + } + + private val assetsDirAccess = AssetsDirectoryAccess(context) + private val fileSystemDirAccess = FilesystemDirectoryAccess(context) + + private fun hasDirId(accessType: AccessType, dirId: Int): Boolean { + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId) + ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId) + } + } + + fun dirOpen(nativeAccessType: Int, path: String?): Int { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (path == null || accessType == null) { + return INVALID_DIR_ID + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path) + ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path) + } + } + + fun dirNext(nativeAccessType: Int, dirId: Int): String { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (accessType == null || !hasDirId(accessType, dirId)) { + Log.w(TAG, "dirNext: Invalid dir id: $dirId") + return "" + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId) + ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId) + } + } + + fun dirClose(nativeAccessType: Int, dirId: Int) { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (accessType == null || !hasDirId(accessType, dirId)) { + Log.w(TAG, "dirClose: Invalid dir id: $dirId") + return + } + + when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId) + ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId) + } + } + + fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (accessType == null || !hasDirId(accessType, dirId)) { + Log.w(TAG, "dirIsDir: Invalid dir id: $dirId") + return false + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId) + ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId) + } + } + + fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (accessType == null || !hasDirId(accessType, dirId)) { + return false + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId) + ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId) + } + } + + fun dirExists(nativeAccessType: Int, path: String?): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (path == null || accessType == null) { + return false + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.dirExists(path) + ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path) + } + } + + fun fileExists(nativeAccessType: Int, path: String?): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (path == null || accessType == null) { + return false + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.fileExists(path) + ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path) + } + } + + fun getDriveCount(nativeAccessType: Int): Int { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0 + return when(accessType) { + ACCESS_RESOURCES -> assetsDirAccess.getDriveCount() + ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount() + } + } + + fun getDrive(nativeAccessType: Int, drive: Int): String { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return "" + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive) + ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive) + } + } + + fun makeDir(nativeAccessType: Int, dir: String): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir) + ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir) + } + } + + fun getSpaceLeft(nativeAccessType: Int): Long { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft() + ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft() + } + } + + fun rename(nativeAccessType: Int, from: String, to: String): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.rename(from, to) + ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to) + } + } + + fun remove(nativeAccessType: Int, filename: String): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.remove(filename) + ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename) + } + } + +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt new file mode 100644 index 0000000000..54fc56fa3e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt @@ -0,0 +1,231 @@ +/*************************************************************************/ +/* FileSystemDirectoryAccess.kt */ +/*************************************************************************/ +/* 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.io.directory + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.os.storage.StorageManager +import android.util.Log +import android.util.SparseArray +import org.godotengine.godot.io.StorageScope +import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID +import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID +import org.godotengine.godot.io.file.FileAccessHandler +import java.io.File + +/** + * Handles directories access with the internal and external filesystem. + */ +internal class FilesystemDirectoryAccess(private val context: Context): + DirectoryAccessHandler.DirectoryAccess { + + companion object { + private val TAG = FilesystemDirectoryAccess::class.java.simpleName + } + + private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0) + + private val storageScopeIdentifier = StorageScope.Identifier(context) + private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager + private var lastDirId = STARTING_DIR_ID + private val dirs = SparseArray<DirData>() + + private fun inScope(path: String): Boolean { + // Directory access is available for shared storage on Android 11+ + // On Android 10, access is also available as long as the `requestLegacyExternalStorage` + // tag is available. + return storageScopeIdentifier.identifyStorageScope(path) != StorageScope.UNKNOWN + } + + override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0 + + override fun dirOpen(path: String): Int { + if (!inScope(path)) { + Log.w(TAG, "Path $path is not accessible.") + return INVALID_DIR_ID + } + + // Check this is a directory. + val dirFile = File(path) + if (!dirFile.isDirectory) { + return INVALID_DIR_ID + } + + // Get the files in the directory + val files = dirFile.listFiles()?: return INVALID_DIR_ID + + // Create the data representing this directory + val dirData = DirData(dirFile, files) + + dirs.put(++lastDirId, dirData) + return lastDirId + } + + override fun dirExists(path: String): Boolean { + if (!inScope(path)) { + Log.w(TAG, "Path $path is not accessible.") + return false + } + + try { + return File(path).isDirectory + } catch (e: SecurityException) { + return false + } + } + + override fun fileExists(path: String) = FileAccessHandler.fileExists(context, storageScopeIdentifier, path) + + override fun dirNext(dirId: Int): String { + val dirData = dirs[dirId] + if (dirData.current >= dirData.files.size) { + dirData.current++ + return "" + } + + return dirData.files[dirData.current++].name + } + + override fun dirClose(dirId: Int) { + dirs.remove(dirId) + } + + override fun dirIsDir(dirId: Int): Boolean { + val dirData = dirs[dirId] + + var index = dirData.current + if (index > 0) { + index-- + } + + if (index >= dirData.files.size) { + return false + } + + return dirData.files[index].isDirectory + } + + override fun isCurrentHidden(dirId: Int): Boolean { + val dirData = dirs[dirId] + + var index = dirData.current + if (index > 0) { + index-- + } + + if (index >= dirData.files.size) { + return false + } + + return dirData.files[index].isHidden + } + + override fun getDriveCount(): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + storageManager.storageVolumes.size + } else { + 0 + } + } + + override fun getDrive(drive: Int): String { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return "" + } + + if (drive < 0 || drive >= storageManager.storageVolumes.size) { + return "" + } + + val storageVolume = storageManager.storageVolumes[drive] + return storageVolume.getDescription(context) + } + + override fun makeDir(dir: String): Boolean { + if (!inScope(dir)) { + Log.w(TAG, "Directory $dir is not accessible.") + return false + } + + try { + val dirFile = File(dir) + return dirFile.isDirectory || dirFile.mkdirs() + } catch (e: SecurityException) { + return false + } + } + + @SuppressLint("UsableSpace") + override fun getSpaceLeft() = context.getExternalFilesDir(null)?.usableSpace ?: 0L + + override fun rename(from: String, to: String): Boolean { + if (!inScope(from) || !inScope(to)) { + Log.w(TAG, "Argument filenames are not accessible:\n" + + "from: $from\n" + + "to: $to") + return false + } + + return try { + val fromFile = File(from) + if (fromFile.isDirectory) { + fromFile.renameTo(File(to)) + } else { + FileAccessHandler.renameFile(context, storageScopeIdentifier, from, to) + } + } catch (e: SecurityException) { + false + } + } + + override fun remove(filename: String): Boolean { + if (!inScope(filename)) { + Log.w(TAG, "Filename $filename is not accessible.") + return false + } + + return try { + val deleteFile = File(filename) + if (deleteFile.exists()) { + if (deleteFile.isDirectory) { + deleteFile.delete() + } else { + FileAccessHandler.removeFile(context, storageScopeIdentifier, filename) + } + } else { + true + } + } catch (e: SecurityException) { + false + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt new file mode 100644 index 0000000000..463dabfb23 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt @@ -0,0 +1,187 @@ +/*************************************************************************/ +/* DataAccess.kt */ +/*************************************************************************/ +/* 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.io.file + +import android.content.Context +import android.os.Build +import android.util.Log +import org.godotengine.godot.io.StorageScope +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.channels.FileChannel +import kotlin.math.max + +/** + * Base class for file IO operations. + * + * Its derived instances provide concrete implementations to handle regular file access, as well + * as file access through the media store API on versions of Android were scoped storage is enabled. + */ +internal abstract class DataAccess(private val filePath: String) { + + companion object { + private val TAG = DataAccess::class.java.simpleName + + fun generateDataAccess( + storageScope: StorageScope, + context: Context, + filePath: String, + accessFlag: FileAccessFlags + ): DataAccess? { + return when (storageScope) { + StorageScope.APP -> FileData(filePath, accessFlag) + + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData(context, filePath, accessFlag) + } else { + null + } + + StorageScope.UNKNOWN -> null + } + } + + fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean { + return when(storageScope) { + StorageScope.APP -> FileData.fileExists(path) + StorageScope.SHARED -> MediaStoreData.fileExists(context, path) + StorageScope.UNKNOWN -> false + } + } + + fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long { + return when(storageScope) { + StorageScope.APP -> FileData.fileLastModified(path) + StorageScope.SHARED -> MediaStoreData.fileLastModified(context, path) + StorageScope.UNKNOWN -> 0L + } + } + + fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean { + return when(storageScope) { + StorageScope.APP -> FileData.delete(path) + StorageScope.SHARED -> MediaStoreData.delete(context, path) + StorageScope.UNKNOWN -> false + } + } + + fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean { + return when(storageScope) { + StorageScope.APP -> FileData.rename(from, to) + StorageScope.SHARED -> MediaStoreData.rename(context, from, to) + StorageScope.UNKNOWN -> false + } + } + } + + protected abstract val fileChannel: FileChannel + internal var endOfFile = false + private set + + fun close() { + try { + fileChannel.close() + } catch (e: IOException) { + Log.w(TAG, "Exception when closing file $filePath.", e) + } + } + + fun flush() { + try { + fileChannel.force(false) + } catch (e: IOException) { + Log.w(TAG, "Exception when flushing file $filePath.", e) + } + } + + fun seek(position: Long) { + try { + fileChannel.position(position) + if (position <= size()) { + endOfFile = false + } + } catch (e: Exception) { + Log.w(TAG, "Exception when seeking file $filePath.", e) + } + } + + fun seekFromEnd(positionFromEnd: Long) { + val positionFromBeginning = max(0, size() - positionFromEnd) + seek(positionFromBeginning) + } + + fun position(): Long { + return try { + fileChannel.position() + } catch (e: IOException) { + Log.w( + TAG, + "Exception when retrieving position for file $filePath.", + e + ) + 0L + } + } + + fun size() = try { + fileChannel.size() + } catch (e: IOException) { + Log.w(TAG, "Exception when retrieving size for file $filePath.", e) + 0L + } + + fun read(buffer: ByteBuffer): Int { + return try { + val readBytes = fileChannel.read(buffer) + endOfFile = readBytes == -1 + || (fileChannel.position() >= fileChannel.size() && fileChannel.size() > 0) + if (readBytes == -1) { + 0 + } else { + readBytes + } + } catch (e: IOException) { + Log.w(TAG, "Exception while reading from file $filePath.", e) + 0 + } + } + + fun write(buffer: ByteBuffer) { + try { + val writtenBytes = fileChannel.write(buffer) + if (writtenBytes > 0) { + endOfFile = false + } + } catch (e: IOException) { + Log.w(TAG, "Exception while writing to file $filePath.", e) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt new file mode 100644 index 0000000000..c6b242a4b6 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* FileAccessFlags.kt */ +/*************************************************************************/ +/* 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.io.file + +/** + * Android representation of Godot native access flags. + */ +internal enum class FileAccessFlags(val nativeValue: Int) { + /** + * Opens the file for read operations. + * The cursor is positioned at the beginning of the file. + */ + READ(1), + + /** + * Opens the file for write operations. + * The file is created if it does not exist, and truncated if it does. + */ + WRITE(2), + + /** + * Opens the file for read and write operations. + * Does not truncate the file. The cursor is positioned at the beginning of the file. + */ + READ_WRITE(3), + + /** + * Opens the file for read and write operations. + * The file is created if it does not exist, and truncated if it does. + * The cursor is positioned at the beginning of the file. + */ + WRITE_READ(7); + + fun getMode(): String { + return when (this) { + READ -> "r" + WRITE -> "w" + READ_WRITE, WRITE_READ -> "rw" + } + } + + fun shouldTruncate(): Boolean { + return when (this) { + READ, READ_WRITE -> false + WRITE, WRITE_READ -> true + } + } + + companion object { + fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? { + for (flag in values()) { + if (flag.nativeValue == modeFlag) { + return flag + } + } + return null + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt new file mode 100644 index 0000000000..04b6772c45 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt @@ -0,0 +1,203 @@ +/*************************************************************************/ +/* FileAccessHandler.kt */ +/*************************************************************************/ +/* 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.io.file + +import android.content.Context +import android.util.Log +import android.util.SparseArray +import org.godotengine.godot.io.StorageScope +import java.io.FileNotFoundException +import java.nio.ByteBuffer + +/** + * Handles regular and media store file access and interactions. + */ +class FileAccessHandler(val context: Context) { + + companion object { + private val TAG = FileAccessHandler::class.java.simpleName + + private const val FILE_NOT_FOUND_ERROR_ID = -1 + private const val INVALID_FILE_ID = 0 + private const val STARTING_FILE_ID = 1 + + internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean { + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + if (storageScope == StorageScope.UNKNOWN) { + return false + } + + return try { + DataAccess.fileExists(storageScope, context, path!!) + } catch (e: SecurityException) { + false + } + } + + internal fun removeFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean { + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + if (storageScope == StorageScope.UNKNOWN) { + return false + } + + return try { + DataAccess.removeFile(storageScope, context, path!!) + } catch (e: Exception) { + false + } + } + + internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String?, to: String?): Boolean { + val storageScope = storageScopeIdentifier.identifyStorageScope(from) + if (storageScope == StorageScope.UNKNOWN) { + return false + } + + return try { + DataAccess.renameFile(storageScope, context, from!!, to!!) + } catch (e: Exception) { + false + } + } + } + + private val storageScopeIdentifier = StorageScope.Identifier(context) + private val files = SparseArray<DataAccess>() + private var lastFileId = STARTING_FILE_ID + + private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0 + + fun fileOpen(path: String?, modeFlags: Int): Int { + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + if (storageScope == StorageScope.UNKNOWN) { + return INVALID_FILE_ID + } + + try { + val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID + val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID + + files.put(++lastFileId, dataAccess) + return lastFileId + } catch (e: FileNotFoundException) { + return FILE_NOT_FOUND_ERROR_ID + } catch (e: Exception) { + Log.w(TAG, "Error while opening $path", e) + return INVALID_FILE_ID + } + } + + fun fileGetSize(fileId: Int): Long { + if (!hasFileId(fileId)) { + return 0L + } + + return files[fileId].size() + } + + fun fileSeek(fileId: Int, position: Long) { + if (!hasFileId(fileId)) { + return + } + + files[fileId].seek(position) + } + + fun fileSeekFromEnd(fileId: Int, position: Long) { + if (!hasFileId(fileId)) { + return + } + + files[fileId].seekFromEnd(position) + } + + fun fileRead(fileId: Int, byteBuffer: ByteBuffer?): Int { + if (!hasFileId(fileId) || byteBuffer == null) { + return 0 + } + + return files[fileId].read(byteBuffer) + } + + fun fileWrite(fileId: Int, byteBuffer: ByteBuffer?) { + if (!hasFileId(fileId) || byteBuffer == null) { + return + } + + files[fileId].write(byteBuffer) + } + + fun fileFlush(fileId: Int) { + if (!hasFileId(fileId)) { + return + } + + files[fileId].flush() + } + + fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path) + + fun fileLastModified(filepath: String?): Long { + val storageScope = storageScopeIdentifier.identifyStorageScope(filepath) + if (storageScope == StorageScope.UNKNOWN) { + return 0L + } + + return try { + DataAccess.fileLastModified(storageScope, context, filepath!!) + } catch (e: SecurityException) { + 0L + } + } + + fun fileGetPosition(fileId: Int): Long { + if (!hasFileId(fileId)) { + return 0L + } + + return files[fileId].position() + } + + fun isFileEof(fileId: Int): Boolean { + if (!hasFileId(fileId)) { + return false + } + + return files[fileId].endOfFile + } + + fun fileClose(fileId: Int) { + if (hasFileId(fileId)) { + files[fileId].close() + files.remove(fileId) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt new file mode 100644 index 0000000000..5af694ad99 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt @@ -0,0 +1,93 @@ +/*************************************************************************/ +/* FileData.kt */ +/*************************************************************************/ +/* 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.io.file + +import java.io.File +import java.io.FileOutputStream +import java.io.RandomAccessFile +import java.nio.channels.FileChannel + +/** + * Implementation of [DataAccess] which handles regular (not scoped) file access and interactions. + */ +internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess(filePath) { + + companion object { + private val TAG = FileData::class.java.simpleName + + fun fileExists(path: String): Boolean { + return try { + File(path).isFile + } catch (e: SecurityException) { + false + } + } + + fun fileLastModified(filepath: String): Long { + return try { + File(filepath).lastModified() + } catch (e: SecurityException) { + 0L + } + } + + fun delete(filepath: String): Boolean { + return try { + File(filepath).delete() + } catch (e: Exception) { + false + } + } + + fun rename(from: String, to: String): Boolean { + return try { + val fromFile = File(from) + fromFile.renameTo(File(to)) + } catch (e: Exception) { + false + } + } + } + + override val fileChannel: FileChannel + + init { + if (accessFlag == FileAccessFlags.WRITE) { + fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel + } else { + fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel + } + + if (accessFlag.shouldTruncate()) { + fileChannel.truncate(0) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt new file mode 100644 index 0000000000..81a7dd1705 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt @@ -0,0 +1,284 @@ +/*************************************************************************/ +/* MediaStoreData.kt */ +/*************************************************************************/ +/* 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.io.file + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.annotation.RequiresApi + +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.nio.channels.FileChannel + +/** + * Implementation of [DataAccess] which handles access and interactions with file and data + * under scoped storage via the MediaStore API. + */ +@RequiresApi(Build.VERSION_CODES.Q) +internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) : + DataAccess(filePath) { + + private data class DataItem( + val id: Long, + val uri: Uri, + val displayName: String, + val relativePath: String, + val size: Int, + val dateModified: Int, + val mediaType: Int + ) + + companion object { + private val TAG = MediaStoreData::class.java.simpleName + + private val COLLECTION = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + + private val PROJECTION = arrayOf( + MediaStore.Files.FileColumns._ID, + MediaStore.Files.FileColumns.DISPLAY_NAME, + MediaStore.Files.FileColumns.RELATIVE_PATH, + MediaStore.Files.FileColumns.SIZE, + MediaStore.Files.FileColumns.DATE_MODIFIED, + MediaStore.Files.FileColumns.MEDIA_TYPE, + ) + + private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " + + " AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?" + + private fun getSelectionByPathArguments(path: String): Array<String> { + return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path)) + } + + private const val SELECTION_BY_ID = "${MediaStore.Files.FileColumns._ID} = ? " + + private fun getSelectionByIdArgument(id: Long) = arrayOf(id.toString()) + + private fun getMediaStoreDisplayName(path: String) = File(path).name + + private fun getMediaStoreRelativePath(path: String): String { + val pathFile = File(path) + val environmentDir = Environment.getExternalStorageDirectory() + var relativePath = (pathFile.parent?.replace(environmentDir.absolutePath, "") ?: "").trim('/') + if (relativePath.isNotBlank()) { + relativePath += "/" + } + return relativePath + } + + private fun queryById(context: Context, id: Long): List<DataItem> { + val query = context.contentResolver.query( + COLLECTION, + PROJECTION, + SELECTION_BY_ID, + getSelectionByIdArgument(id), + null + ) + return dataItemFromCursor(query) + } + + private fun queryByPath(context: Context, path: String): List<DataItem> { + val query = context.contentResolver.query( + COLLECTION, + PROJECTION, + SELECTION_BY_PATH, + getSelectionByPathArguments(path), + null + ) + return dataItemFromCursor(query) + } + + private fun dataItemFromCursor(query: Cursor?): List<DataItem> { + query?.use { cursor -> + cursor.count + if (cursor.count == 0) { + return emptyList() + } + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) + val displayNameColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME) + val relativePathColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.RELATIVE_PATH) + val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE) + val dateModifiedColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED) + val mediaTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) + + val result = ArrayList<DataItem>() + while (cursor.moveToNext()) { + val id = cursor.getLong(idColumn) + result.add( + DataItem( + id, + ContentUris.withAppendedId(COLLECTION, id), + cursor.getString(displayNameColumn), + cursor.getString(relativePathColumn), + cursor.getInt(sizeColumn), + cursor.getInt(dateModifiedColumn), + cursor.getInt(mediaTypeColumn) + ) + ) + } + return result + } + return emptyList() + } + + private fun addFile(context: Context, path: String): DataItem? { + val fileDetails = ContentValues().apply { + put(MediaStore.Files.FileColumns._ID, 0) + put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(path)) + put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(path)) + } + + context.contentResolver.insert(COLLECTION, fileDetails) ?: return null + + // File was successfully added, let's retrieve its info + val infos = queryByPath(context, path) + if (infos.isEmpty()) { + return null + } + + return infos[0] + } + + fun delete(context: Context, path: String): Boolean { + val itemsToDelete = queryByPath(context, path) + if (itemsToDelete.isEmpty()) { + return false + } + + val resolver = context.contentResolver + var itemsDeleted = 0 + for (item in itemsToDelete) { + itemsDeleted += resolver.delete(item.uri, null, null) + } + + return itemsDeleted > 0 + } + + fun fileExists(context: Context, path: String): Boolean { + return queryByPath(context, path).isNotEmpty() + } + + fun fileLastModified(context: Context, path: String): Long { + val result = queryByPath(context, path) + if (result.isEmpty()) { + return 0L + } + + val dataItem = result[0] + return dataItem.dateModified.toLong() + } + + fun rename(context: Context, from: String, to: String): Boolean { + // Ensure the source exists. + val sources = queryByPath(context, from) + if (sources.isEmpty()) { + return false + } + + // Take the first source + val source = sources[0] + + // Set up the updated values + val updatedDetails = ContentValues().apply { + put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(to)) + put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(to)) + } + + val updated = context.contentResolver.update( + source.uri, + updatedDetails, + SELECTION_BY_ID, + getSelectionByIdArgument(source.id) + ) + return updated > 0 + } + } + + private val id: Long + private val uri: Uri + override val fileChannel: FileChannel + + init { + val contentResolver = context.contentResolver + val dataItems = queryByPath(context, filePath) + + val dataItem = when (accessFlag) { + FileAccessFlags.READ -> { + // The file should already exist + if (dataItems.isEmpty()) { + throw FileNotFoundException("Unable to access file $filePath") + } + + val dataItem = dataItems[0] + dataItem + } + + FileAccessFlags.WRITE, FileAccessFlags.READ_WRITE, FileAccessFlags.WRITE_READ -> { + // Create the file if it doesn't exist + val dataItem = if (dataItems.isEmpty()) { + addFile(context, filePath) + } else { + dataItems[0] + } + + if (dataItem == null) { + throw FileNotFoundException("Unable to access file $filePath") + } + dataItem + } + } + + id = dataItem.id + uri = dataItem.uri + + val parcelFileDescriptor = contentResolver.openFileDescriptor(uri, accessFlag.getMode()) + ?: throw IllegalStateException("Unable to access file descriptor") + fileChannel = if (accessFlag == FileAccessFlags.READ) { + FileInputStream(parcelFileDescriptor.fileDescriptor).channel + } else { + FileOutputStream(parcelFileDescriptor.fileDescriptor).channel + } + + if (accessFlag.shouldTruncate()) { + fileChannel.truncate(0) + } + } +} 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/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java index e5b4f41153..57db0709f0 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java @@ -32,10 +32,14 @@ package org.godotengine.godot.utils; import android.Manifest; import android.app.Activity; +import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionInfo; +import android.net.Uri; import android.os.Build; +import android.os.Environment; +import android.provider.Settings; import android.util.Log; import androidx.core.content.ContextCompat; @@ -53,7 +57,8 @@ public final class PermissionsUtil { static final int REQUEST_RECORD_AUDIO_PERMISSION = 1; static final int REQUEST_CAMERA_PERMISSION = 2; static final int REQUEST_VIBRATE_PERMISSION = 3; - static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001; + public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001; + public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002; private PermissionsUtil() { } @@ -108,13 +113,26 @@ public final class PermissionsUtil { if (manifestPermissions.length == 0) return true; - List<String> dangerousPermissions = new ArrayList<>(); + List<String> requestedPermissions = new ArrayList<>(); for (String manifestPermission : manifestPermissions) { try { - PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); - int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; - if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) { - dangerousPermissions.add(manifestPermission); + if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName()))); + activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); + } catch (Exception ignored) { + Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); + } + } + } else { + PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); + int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) { + requestedPermissions.add(manifestPermission); + } } } catch (PackageManager.NameNotFoundException e) { // Skip this permission and continue. @@ -122,13 +140,12 @@ public final class PermissionsUtil { } } - if (dangerousPermissions.isEmpty()) { + if (requestedPermissions.isEmpty()) { // If list is empty, all of dangerous permissions were granted. return true; } - String[] requestedPermissions = dangerousPermissions.toArray(new String[0]); - activity.requestPermissions(requestedPermissions, REQUEST_ALL_PERMISSION_REQ_CODE); + activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE); return false; } @@ -148,13 +165,19 @@ public final class PermissionsUtil { if (manifestPermissions.length == 0) return manifestPermissions; - List<String> dangerousPermissions = new ArrayList<>(); + List<String> grantedPermissions = new ArrayList<>(); for (String manifestPermission : manifestPermissions) { try { - PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); - int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; - if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) { - dangerousPermissions.add(manifestPermission); + if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) { + grantedPermissions.add(manifestPermission); + } + } else { + PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); + int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) { + grantedPermissions.add(manifestPermission); + } } } catch (PackageManager.NameNotFoundException e) { // Skip this permission and continue. @@ -162,7 +185,7 @@ public final class PermissionsUtil { } } - return dangerousPermissions.toArray(new String[0]); + return grantedPermissions.toArray(new String[0]); } /** @@ -177,7 +200,7 @@ public final class PermissionsUtil { if (permission.equals(p)) return true; } - } catch (PackageManager.NameNotFoundException e) { + } catch (PackageManager.NameNotFoundException ignored) { } return false; diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java new file mode 100644 index 0000000000..2cc37b627a --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java @@ -0,0 +1,141 @@ +// clang-format off + +/* Third-party library. + * Upstream: https://github.com/JakeWharton/ProcessPhoenix + * Commit: 12cb27c2cc9c3fc555e97f2db89e571667de82c4 + */ + +/* + * Copyright (C) 2014 Jake Wharton + * + * 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.utils; + +import android.app.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Process; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; + +/** + * Process Phoenix facilitates restarting your application process. This should only be used for + * things like fundamental state changes in your debug builds (e.g., changing from staging to + * production). + * <p> + * Trigger process recreation by calling {@link #triggerRebirth} with a {@link Context} instance. + */ +public final class ProcessPhoenix extends Activity { + private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents"; + private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid"; + + /** + * Call to restart the application process using the {@linkplain Intent#CATEGORY_DEFAULT default} + * activity as an intent. + * <p> + * Behavior of the current process after invoking this method is undefined. + */ + public static void triggerRebirth(Context context) { + triggerRebirth(context, getRestartIntent(context)); + } + + /** + * Call to restart the application process using the specified intents. + * <p> + * Behavior of the current process after invoking this method is undefined. + */ + public static void triggerRebirth(Context context, Intent... nextIntents) { + if (nextIntents.length < 1) { + throw new IllegalArgumentException("intents cannot be empty"); + } + // create a new task for the first activity. + nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); + + Intent intent = new Intent(context, ProcessPhoenix.class); + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context. + intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents))); + intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid()); + context.startActivity(intent); + } + + // -- GODOT start -- + /** + * Finish the activity and kill its process + */ + public static void forceQuit(Activity activity) { + forceQuit(activity, Process.myPid()); + } + + /** + * Finish the activity and kill its process + * @param activity + * @param pid + */ + public static void forceQuit(Activity activity, int pid) { + Process.killProcess(pid); // Kill original main process + activity.finish(); + Runtime.getRuntime().exit(0); // Kill kill kill! + } + + // -- GODOT end -- + + private static Intent getRestartIntent(Context context) { + String packageName = context.getPackageName(); + Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); + if (defaultIntent != null) { + return defaultIntent; + } + + throw new IllegalStateException("Unable to determine default activity for " + + packageName + + ". Does an activity specify the DEFAULT category in its intent filter?"); + } + + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // -- GODOT start -- + ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS); + startActivities(intents.toArray(new Intent[intents.size()])); + forceQuit(this, getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); + // -- GODOT end -- + } + + /** + * Checks if the current process is a temporary Phoenix Process. + * This can be used to avoid initialisation of unused resources or to prevent running code that + * is not multi-process ready. + * + * @return true if the current process is a temporary Phoenix Process + */ + public static boolean isPhoenixProcess(Context context) { + int currentPid = Process.myPid(); + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + List<ActivityManager.RunningAppProcessInfo> runningProcesses = manager.getRunningAppProcesses(); + if (runningProcesses != null) { + for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) { + if (processInfo.pid == currentPid && processInfo.processName.endsWith(":phoenix")) { + return true; + } + } + } + return false; + } +} 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 34925684da..711f7cd502 100644 --- a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt +++ b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt @@ -15,5 +15,6 @@ file(GLOB_RECURSE HEADERS ${GODOT_ROOT_DIR}/*.h**) add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC - ${GODOT_ROOT_DIR} - ${GODOT_ROOT_DIR}/modules/gdnative/include) + ${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..5e810ae1ba 100644 --- a/platform/android/java/nativeSrcsConfigs/build.gradle +++ b/platform/android/java/nativeSrcsConfigs/build.gradle @@ -1,11 +1,13 @@ // Non functional android library used to provide Android Studio editor support to the project. plugins { id 'com.android.library' + id 'org.jetbrains.kotlin.android' } android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion defaultConfig { minSdkVersion versions.minSdk @@ -17,6 +19,10 @@ android { targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + packagingOptions { exclude 'META-INF/LICENSE' exclude 'META-INF/NOTICE' @@ -28,8 +34,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 new file mode 100644 index 0000000000..32b749e493 --- /dev/null +++ b/platform/android/java/scripts/publish-module.gradle @@ -0,0 +1,69 @@ +apply plugin: 'maven-publish' +apply plugin: 'signing' + +group = ossrhGroupId +version = PUBLISH_VERSION + +afterEvaluate { + publishing { + publications { + 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 + + // Mostly self-explanatory metadata + pom { + name = PUBLISH_ARTIFACT_ID + description = 'Godot Engine Android Library' + url = 'https://godotengine.org/' + licenses { + license { + name = 'MIT License' + url = 'https://github.com/godotengine/godot/blob/master/LICENSE.txt' + } + } + developers { + developer { + id = 'm4gr3d' + name = 'Fredia Huya-Kouadio' + email = 'fhuyakou@gmail.com' + } + developer { + id = 'reduz' + name = 'Juan Linietsky' + email = 'reduzio@gmail.com' + } + developer { + id = 'akien-mga' + name = 'Rémi Verschelde' + email = 'rverschelde@gmail.com' + } + // Add all other devs here... + } + + // Version control info - if you're using GitHub, follow the + // format as seen here + scm { + connection = 'scm:git:github.com/godotengine/godot.git' + developerConnection = 'scm:git:ssh://github.com/godotengine/godot.git' + url = 'https://github.com/godotengine/godot/tree/master' + } + } + } + } + } +} + +signing { + useInMemoryPgpKeys( + rootProject.ext["signing.keyId"], + rootProject.ext["signing.key"], + rootProject.ext["signing.password"], + ) + sign publishing.publications +} diff --git a/platform/android/java/scripts/publish-root.gradle b/platform/android/java/scripts/publish-root.gradle new file mode 100644 index 0000000000..ae88487c34 --- /dev/null +++ b/platform/android/java/scripts/publish-root.gradle @@ -0,0 +1,39 @@ +// Create variables with empty default values +ext["signing.keyId"] = '' +ext["signing.password"] = '' +ext["signing.key"] = '' +ext["ossrhGroupId"] = '' +ext["ossrhUsername"] = '' +ext["ossrhPassword"] = '' +ext["sonatypeStagingProfileId"] = '' + +File secretPropsFile = project.rootProject.file('local.properties') +if (secretPropsFile.exists()) { + // Read local.properties file first if it exists + Properties p = new Properties() + new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } + p.each { name, value -> ext[name] = value } +} else { + // Use system environment variables + ext["ossrhGroupId"] = System.getenv('OSSRH_GROUP_ID') + ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') + ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') + ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') + ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') + ext["signing.password"] = System.getenv('SIGNING_PASSWORD') + ext["signing.key"] = System.getenv('SIGNING_KEY') +} + +// Set up Sonatype repository +nexusPublishing { + repositories { + sonatype { + stagingProfileId = sonatypeStagingProfileId + username = ossrhUsername + password = ossrhPassword + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + } + } +} + diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index 584b626900..466ffebf22 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -1,9 +1,25 @@ // Configure the root project. +pluginManagement { + apply from: 'app/config.gradle' + + plugins { + id 'com.android.application' version versions.androidGradlePlugin + id 'com.android.library' version versions.androidGradlePlugin + id 'org.jetbrains.kotlin.android' version versions.kotlinVersion + id 'io.github.gradle-nexus.publish-plugin' version versions.nexusPublishVersion + } + repositories { + gradlePluginPortal() + google() + } +} + 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_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp index 7c788b4dc4..349ae704f9 100644 --- a/platform/android/java_class_wrapper.cpp +++ b/platform/android/java_class_wrapper.cpp @@ -34,16 +34,16 @@ #include "thread_jandroid.h" bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error, Variant &ret) { - Map<StringName, List<MethodInfo>>::Element *M = methods.find(p_method); + HashMap<StringName, List<MethodInfo>>::Iterator M = methods.find(p_method); if (!M) { return false; } JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); MethodInfo *method = nullptr; - for (MethodInfo &E : M->get()) { + for (MethodInfo &E : M->value) { if (!p_instance && !E._static) { r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; continue; @@ -485,14 +485,14 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, return success; } -Variant JavaClass::call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { +Variant JavaClass::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { Variant ret; bool found = _call_method(nullptr, p_method, p_args, p_argcount, r_error, ret); if (found) { return ret; } - return RefCounted::call(p_method, p_args, p_argcount, r_error); + return RefCounted::callp(p_method, p_args, p_argcount, r_error); } JavaClass::JavaClass() { @@ -500,7 +500,7 @@ JavaClass::JavaClass() { ///////////////////// -Variant JavaObject::call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { +Variant JavaObject::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { return Variant(); } @@ -971,14 +971,14 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) { } JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, Ref<JavaClass>()); + ERR_FAIL_NULL_V(env, Ref<JavaClass>()); jclass bclass = env->FindClass(p_class.utf8().get_data()); - ERR_FAIL_COND_V(!bclass, Ref<JavaClass>()); + ERR_FAIL_NULL_V(bclass, Ref<JavaClass>()); jobjectArray methods = (jobjectArray)env->CallObjectMethod(bclass, getDeclaredMethods); - ERR_FAIL_COND_V(!methods, Ref<JavaClass>()); + ERR_FAIL_NULL_V(methods, Ref<JavaClass>()); Ref<JavaClass> java_class = memnew(JavaClass); @@ -1061,7 +1061,7 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) { if (E->get().param_types.size() != mi.param_types.size()) { continue; } - bool valid = true; + bool this_valid = true; for (int j = 0; j < E->get().param_types.size(); j++) { Variant::Type _new; float new_l; @@ -1070,14 +1070,14 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) { JavaClass::_convert_to_variant_type(E->get().param_types[j], existing, existing_l); JavaClass::_convert_to_variant_type(mi.param_types[j], _new, new_l); if (_new != existing) { - valid = false; + this_valid = false; break; } new_likeliness += new_l; existing_likeliness = existing_l; } - if (!valid) { + if (!this_valid) { continue; } @@ -1155,10 +1155,10 @@ JavaClassWrapper::JavaClassWrapper(jobject p_activity) { singleton = this; JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); - jclass activityClass = env->FindClass("android/app/Activity"); - jmethodID getClassLoader = env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;"); + jclass activity = env->FindClass("android/app/Activity"); + jmethodID getClassLoader = env->GetMethodID(activity, "getClassLoader", "()Ljava/lang/ClassLoader;"); classLoader = env->CallObjectMethod(p_activity, getClassLoader); classLoader = (jclass)env->NewGlobalRef(classLoader); jclass classLoaderClass = env->FindClass("java/lang/ClassLoader"); @@ -1168,18 +1168,18 @@ JavaClassWrapper::JavaClassWrapper(jobject p_activity) { getDeclaredMethods = env->GetMethodID(bclass, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;"); getFields = env->GetMethodID(bclass, "getFields", "()[Ljava/lang/reflect/Field;"); Class_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); - // + bclass = env->FindClass("java/lang/reflect/Method"); getParameterTypes = env->GetMethodID(bclass, "getParameterTypes", "()[Ljava/lang/Class;"); getReturnType = env->GetMethodID(bclass, "getReturnType", "()Ljava/lang/Class;"); getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); getModifiers = env->GetMethodID(bclass, "getModifiers", "()I"); - /// + bclass = env->FindClass("java/lang/reflect/Field"); Field_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); Field_getModifiers = env->GetMethodID(bclass, "getModifiers", "()I"); Field_get = env->GetMethodID(bclass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); - // each + bclass = env->FindClass("java/lang/Boolean"); Boolean_booleanValue = env->GetMethodID(bclass, "booleanValue", "()Z"); diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index d6e3ad90b1..b71c6ef1e6 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -31,6 +31,8 @@ #include "java_godot_io_wrapper.h" #include "core/error/error_list.h" +#include "core/math/rect2.h" +#include "core/variant/variant.h" // JNIEnv is only valid within the thread it belongs to, in a multi threading environment // we can't cache it. @@ -51,11 +53,13 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc _open_URI = p_env->GetMethodID(cls, "openURI", "(Ljava/lang/String;)I"); _get_cache_dir = p_env->GetMethodID(cls, "getCacheDir", "()Ljava/lang/String;"); _get_data_dir = p_env->GetMethodID(cls, "getDataDir", "()Ljava/lang/String;"); + _get_display_cutouts = p_env->GetMethodID(cls, "getDisplayCutouts", "()[I"), + _get_display_safe_area = p_env->GetMethodID(cls, "getDisplaySafeArea", "()[I"), _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;"); _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;ZIII)V"); _hide_keyboard = p_env->GetMethodID(cls, "hideKeyboard", "()V"); @@ -76,7 +80,7 @@ jobject GodotIOJavaWrapper::get_instance() { Error GodotIOJavaWrapper::open_uri(const String &p_uri) { if (_open_URI) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, ERR_UNAVAILABLE); + ERR_FAIL_NULL_V(env, ERR_UNAVAILABLE); jstring jStr = env->NewStringUTF(p_uri.utf8().get_data()); return env->CallIntMethod(godot_io_instance, _open_URI, jStr) ? ERR_CANT_OPEN : OK; } else { @@ -87,7 +91,7 @@ Error GodotIOJavaWrapper::open_uri(const String &p_uri) { String GodotIOJavaWrapper::get_cache_dir() { if (_get_cache_dir) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_cache_dir); return jstring_to_string(s, env); } else { @@ -98,7 +102,7 @@ String GodotIOJavaWrapper::get_cache_dir() { String GodotIOJavaWrapper::get_user_data_dir() { if (_get_data_dir) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_data_dir); return jstring_to_string(s, env); } else { @@ -109,7 +113,7 @@ String GodotIOJavaWrapper::get_user_data_dir() { String GodotIOJavaWrapper::get_locale() { if (_get_locale) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_locale); return jstring_to_string(s, env); } else { @@ -120,7 +124,7 @@ String GodotIOJavaWrapper::get_locale() { String GodotIOJavaWrapper::get_model() { if (_get_model) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_model); return jstring_to_string(s, env); } else { @@ -131,13 +135,23 @@ String GodotIOJavaWrapper::get_model() { int GodotIOJavaWrapper::get_screen_dpi() { if (_get_screen_DPI) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 160); + ERR_FAIL_NULL_V(env, 160); return env->CallIntMethod(godot_io_instance, _get_screen_DPI); } else { return 160; } } +float GodotIOJavaWrapper::get_scaled_density() { + if (_get_scaled_density) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, 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(); @@ -151,24 +165,44 @@ float GodotIOJavaWrapper::get_screen_refresh_rate(float fallback) { return fallback; } -void GodotIOJavaWrapper::screen_get_usable_rect(int (&p_rect_xywh)[4]) { - if (_screen_get_usable_rect) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _screen_get_usable_rect); - ERR_FAIL_COND(env->GetArrayLength(returnArray) != 4); - jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); - for (int i = 0; i < 4; i++) { - p_rect_xywh[i] = arrayBody[i]; - } - env->ReleaseIntArrayElements(returnArray, arrayBody, 0); +Array GodotIOJavaWrapper::get_display_cutouts() { + Array result; + ERR_FAIL_NULL_V(_get_display_cutouts, result); + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, result); + jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _get_display_cutouts); + jint arrayLength = env->GetArrayLength(returnArray); + jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); + int cutouts = arrayLength / 4; + for (int i = 0; i < cutouts; i++) { + int x = arrayBody[i * 4]; + int y = arrayBody[i * 4 + 1]; + int width = arrayBody[i * 4 + 2]; + int height = arrayBody[i * 4 + 3]; + Rect2 cutout(x, y, width, height); + result.append(cutout); } + env->ReleaseIntArrayElements(returnArray, arrayBody, 0); + return result; +} + +Rect2i GodotIOJavaWrapper::get_display_safe_area() { + Rect2i result; + ERR_FAIL_NULL_V(_get_display_safe_area, result); + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, result); + jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _get_display_safe_area); + ERR_FAIL_COND_V(env->GetArrayLength(returnArray) != 4, result); + jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); + result = Rect2i(arrayBody[0], arrayBody[1], arrayBody[2], arrayBody[3]); + env->ReleaseIntArrayElements(returnArray, arrayBody, 0); + return result; } String GodotIOJavaWrapper::get_unique_id() { if (_get_unique_id) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_unique_id); return jstring_to_string(s, env); } else { @@ -177,13 +211,13 @@ String GodotIOJavaWrapper::get_unique_id() { } bool GodotIOJavaWrapper::has_vk() { - return (_show_keyboard != 0) && (_hide_keyboard != 0); + return (_show_keyboard != nullptr) && (_hide_keyboard != nullptr); } void GodotIOJavaWrapper::show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { if (_show_keyboard) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); jstring jStr = env->NewStringUTF(p_existing.utf8().get_data()); env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end); } @@ -192,7 +226,7 @@ void GodotIOJavaWrapper::show_vk(const String &p_existing, bool p_multiline, int void GodotIOJavaWrapper::hide_vk() { if (_hide_keyboard) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(godot_io_instance, _hide_keyboard); } } @@ -200,7 +234,7 @@ void GodotIOJavaWrapper::hide_vk() { void GodotIOJavaWrapper::set_screen_orientation(int p_orient) { if (_set_screen_orientation) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(godot_io_instance, _set_screen_orientation, p_orient); } } @@ -208,7 +242,7 @@ void GodotIOJavaWrapper::set_screen_orientation(int p_orient) { int GodotIOJavaWrapper::get_screen_orientation() { if (_get_screen_orientation) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_NULL_V(env, 0); return env->CallIntMethod(godot_io_instance, _get_screen_orientation); } else { return 0; @@ -218,7 +252,7 @@ int GodotIOJavaWrapper::get_screen_orientation() { String GodotIOJavaWrapper::get_system_dir(int p_dir, bool p_shared_storage) { if (_get_system_dir) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String(".")); + ERR_FAIL_NULL_V(env, String(".")); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_system_dir, p_dir, p_shared_storage); return jstring_to_string(s, env); } else { diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index 38a2b710a9..aec7d1db97 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -37,6 +37,8 @@ #include <android/log.h> #include <jni.h> +#include "core/math/rect2i.h" +#include "core/variant/array.h" #include "string_android.h" // Class that makes functions in java/src/org/godotengine/godot/GodotIO.java callable from C++ @@ -48,11 +50,13 @@ private: jmethodID _open_URI = 0; jmethodID _get_cache_dir = 0; jmethodID _get_data_dir = 0; + jmethodID _get_display_cutouts = 0; + jmethodID _get_display_safe_area = 0; 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; jmethodID _show_keyboard = 0; jmethodID _hide_keyboard = 0; @@ -72,8 +76,10 @@ 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]); + Array get_display_cutouts(); + Rect2i get_display_safe_area(); String get_unique_id(); bool has_vk(); void show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end); @@ -85,4 +91,4 @@ public: String get_system_dir(int p_dir, bool p_shared_storage); }; -#endif /* !JAVA_GODOT_IO_WRAPPER_H */ +#endif // JAVA_GODOT_IO_WRAPPER_H diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index dd4fa9de7b..f4de4acfad 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -43,12 +43,14 @@ #include "dir_access_jandroid.h" #include "display_server_android.h" #include "file_access_android.h" +#include "file_access_filesystem_jandroid.h" #include "jni_utils.h" #include "main/main.h" #include "net_socket_android.h" #include "os_android.h" #include "string_android.h" #include "thread_jandroid.h" +#include "tts_android.h" #include <android/input.h> #include <unistd.h> @@ -61,7 +63,6 @@ static AndroidInputHandler *input_handler = nullptr; static GodotJavaWrapper *godot_java = nullptr; static GodotIOJavaWrapper *godot_io_java = nullptr; -static bool initialized = false; static SafeNumeric<int> step; // Shared between UI and render threads static Size2 new_size; @@ -78,15 +79,13 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei } } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject godot_instance, jobject p_asset_manager, jboolean p_use_apk_expansion) { - initialized = true; - +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion, jobject p_godot_tts) { JavaVM *jvm; env->GetJavaVM(&jvm); // create our wrapper classes - godot_java = new GodotJavaWrapper(env, activity, godot_instance); - godot_io_java = new GodotIOJavaWrapper(env, godot_java->get_member_object("io", "Lorg/godotengine/godot/GodotIO;", env)); + godot_java = new GodotJavaWrapper(env, p_activity, p_godot_instance); + godot_io_java = new GodotIOJavaWrapper(env, p_godot_io); init_thread_jandroid(jvm, env); @@ -94,19 +93,21 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *en FileAccessAndroid::asset_manager = AAssetManager_fromJava(env, amgr); - DirAccessJAndroid::setup(godot_io_java->get_instance()); - NetSocketAndroid::setup(godot_java->get_member_object("netUtils", "Lorg/godotengine/godot/utils/GodotNetUtils;", env)); + DirAccessJAndroid::setup(p_directory_access_handler); + FileAccessFilesystemJAndroid::setup(p_file_access_handler); + NetSocketAndroid::setup(p_net_utils); + TTS_Android::setup(p_godot_tts); os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion); - char wd[500]; - getcwd(wd, 500); - godot_java->on_video_init(env); } 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 +118,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; } } @@ -138,7 +140,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc for (int i = 0; i < cmdlen; i++) { jstring string = (jstring)env->GetObjectArrayElement(p_cmdline, i); - const char *rawString = env->GetStringUTFChars(string, 0); + const char *rawString = env->GetStringUTFChars(string, nullptr); cmdline[i] = rawString; j_cmdline[i] = string; @@ -146,7 +148,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) { @@ -157,6 +159,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc memfree(cmdline); } + // Note: --help and --version return ERR_HELP, but this should be translated to 0 if exit codes are propagated. if (err != OK) { return; // should exit instead and print the error } @@ -209,9 +212,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 +227,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 +246,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) { @@ -299,15 +309,6 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubleTap(JNIEnv *env } // Called on the UI thread -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jclass clazz, jint p_x, jint p_y) { - if (step.get() <= 0) { - return; - } - - input_handler->process_scroll(Point2(p_x, p_y)); -} - -// Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed) { if (step.get() <= 0) { return; @@ -423,7 +424,7 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv * JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params) { Object *obj = ObjectDB::get_instance(ObjectID(ID)); - ERR_FAIL_COND(!obj); + ERR_FAIL_NULL(obj); int res = env->PushLocalFrame(16); ERR_FAIL_COND(res != 0); @@ -434,27 +435,26 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *en Variant *vlist = (Variant *)alloca(sizeof(Variant) * count); Variant **vptr = (Variant **)alloca(sizeof(Variant *) * count); for (int i = 0; i < count; i++) { - jobject obj = env->GetObjectArrayElement(params, i); + jobject jobj = env->GetObjectArrayElement(params, i); Variant v; - if (obj) { - v = _jobject_to_variant(env, obj); + if (jobj) { + v = _jobject_to_variant(env, jobj); } memnew_placement(&vlist[i], Variant); vlist[i] = v; vptr[i] = &vlist[i]; - env->DeleteLocalRef(obj); + env->DeleteLocalRef(jobj); } Callable::CallError err; - obj->call(str_method, (const Variant **)vptr, count, err); - // something + obj->callp(str_method, (const Variant **)vptr, count, err); env->PopLocalFrame(nullptr); } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params) { Object *obj = ObjectDB::get_instance(ObjectID(ID)); - ERR_FAIL_COND(!obj); + ERR_FAIL_NULL(obj); int res = env->PushLocalFrame(16); ERR_FAIL_COND(res != 0); @@ -462,19 +462,21 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv * String str_method = jstring_to_string(method, env); int count = env->GetArrayLength(params); - Variant args[VARIANT_ARG_MAX]; - for (int i = 0; i < MIN(count, VARIANT_ARG_MAX); i++) { - jobject obj = env->GetObjectArrayElement(params, i); - if (obj) { - args[i] = _jobject_to_variant(env, obj); + Variant *args = (Variant *)alloca(sizeof(Variant) * count); + const Variant **argptrs = (const Variant **)alloca(sizeof(Variant *) * count); + + for (int i = 0; i < count; i++) { + jobject jobj = env->GetObjectArrayElement(params, i); + if (jobj) { + args[i] = _jobject_to_variant(env, jobj); } - env->DeleteLocalRef(obj); + env->DeleteLocalRef(jobj); + argptrs[i] = &args[i]; } - static_assert(VARIANT_ARG_MAX == 8, "This code needs to be updated if VARIANT_ARG_MAX != 8"); - obj->call_deferred(str_method, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); - // something + MessageQueue::get_singleton()->push_callp(obj, str_method, (const Variant **)argptrs, count); + env->PopLocalFrame(nullptr); } @@ -494,6 +496,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..de16f197b8 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -37,12 +37,13 @@ // These functions can be called from within JAVA and are the means by which our JAVA implementation calls back into our C++ code. // See java/src/org/godotengine/godot/GodotLib.java for the JAVA side of this (yes that's why we have the long names) extern "C" { -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject godot_instance, jobject p_asset_manager, jboolean p_use_apk_expansion); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion, jobject p_godot_tts); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz); 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); @@ -50,7 +51,6 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3FI(JNIEn JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3FIFF(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); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jclass clazz, jint p_type, jfloat p_x, jfloat p_y); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubleTap(JNIEnv *env, jclass clazz, jint p_button_mask, jint p_x, jint p_y); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jclass clazz, jint p_x, jint p_y); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_keycode, jint p_scancode, jint p_unicode_char, jboolean p_pressed); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jclass clazz, jint p_device, jint p_axis, jfloat p_value); @@ -71,4 +71,4 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); } -#endif /* !JAVA_GODOT_LIB_JNI_H */ +#endif // JAVA_GODOT_LIB_JNI_H diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp index 5605d834fa..0153ba96fc 100644 --- a/platform/android/java_godot_view_wrapper.cpp +++ b/platform/android/java_godot_view_wrapper.cpp @@ -34,7 +34,7 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); _godot_view = env->NewGlobalRef(godot_view); @@ -48,27 +48,27 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) { } void GodotJavaViewWrapper::request_pointer_capture() { - if (_request_pointer_capture != 0) { + if (_request_pointer_capture != nullptr) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(_godot_view, _request_pointer_capture); } } void GodotJavaViewWrapper::release_pointer_capture() { - if (_request_pointer_capture != 0) { + if (_request_pointer_capture != nullptr) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(_godot_view, _release_pointer_capture); } } void GodotJavaViewWrapper::set_pointer_icon(int pointer_type) { - if (_set_pointer_icon != 0) { + if (_set_pointer_icon != nullptr) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(_godot_view, _set_pointer_icon, pointer_type); } @@ -76,7 +76,7 @@ void GodotJavaViewWrapper::set_pointer_icon(int pointer_type) { GodotJavaViewWrapper::~GodotJavaViewWrapper() { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->DeleteGlobalRef(_godot_view); env->DeleteGlobalRef(_cls); diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h index 6e02c26ae2..c52f459d64 100644 --- a/platform/android/java_godot_view_wrapper.h +++ b/platform/android/java_godot_view_wrapper.h @@ -28,8 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef GODOT_JAVA_GODOT_VIEW_WRAPPER_H -#define GODOT_JAVA_GODOT_VIEW_WRAPPER_H +#ifndef JAVA_GODOT_VIEW_WRAPPER_H +#define JAVA_GODOT_VIEW_WRAPPER_H #include <android/log.h> #include <jni.h> @@ -57,4 +57,4 @@ public: ~GodotJavaViewWrapper(); }; -#endif //GODOT_JAVA_GODOT_VIEW_WRAPPER_H +#endif // JAVA_GODOT_VIEW_WRAPPER_H diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 754267c834..e3456fe4e4 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;"); @@ -95,9 +96,7 @@ jobject GodotJavaWrapper::get_member_object(const char *p_name, const char *p_cl if (p_env == nullptr) { p_env = get_jni_env(); } - - ERR_FAIL_COND_V(p_env == nullptr, nullptr); - + ERR_FAIL_NULL_V(p_env, nullptr); jfieldID fid = p_env->GetStaticFieldID(godot_class, p_name, p_class); return p_env->GetStaticObjectField(godot_class, fid); } else { @@ -108,8 +107,7 @@ jobject GodotJavaWrapper::get_member_object(const char *p_name, const char *p_cl jobject GodotJavaWrapper::get_class_loader() { if (_get_class_loader) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, nullptr); - + ERR_FAIL_NULL_V(env, nullptr); return env->CallObjectMethod(activity, _get_class_loader); } else { return nullptr; @@ -121,8 +119,7 @@ GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() { return _godot_view; } JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, nullptr); - + ERR_FAIL_NULL_V(env, nullptr); jmethodID godot_view_getter = env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); _godot_view = new GodotJavaViewWrapper(env->CallObjectMethod(godot_instance, godot_view_getter)); return _godot_view; @@ -133,8 +130,7 @@ void GodotJavaWrapper::on_video_init(JNIEnv *p_env) { if (p_env == nullptr) { p_env = get_jni_env(); } - ERR_FAIL_COND(p_env == nullptr); - + ERR_FAIL_NULL(p_env); p_env->CallVoidMethod(godot_instance, _on_video_init); } } @@ -153,7 +149,7 @@ void GodotJavaWrapper::on_godot_main_loop_started(JNIEnv *p_env) { if (p_env == nullptr) { p_env = get_jni_env(); } - ERR_FAIL_COND(p_env == nullptr); + ERR_FAIL_NULL(p_env); p_env->CallVoidMethod(godot_instance, _on_godot_main_loop_started); } } @@ -163,8 +159,7 @@ void GodotJavaWrapper::restart(JNIEnv *p_env) { if (p_env == nullptr) { p_env = get_jni_env(); } - ERR_FAIL_COND(p_env == nullptr); - + ERR_FAIL_NULL(p_env); p_env->CallVoidMethod(godot_instance, _restart); } } @@ -174,8 +169,7 @@ void GodotJavaWrapper::force_quit(JNIEnv *p_env) { if (p_env == nullptr) { p_env = get_jni_env(); } - ERR_FAIL_COND(p_env == nullptr); - + ERR_FAIL_NULL(p_env); p_env->CallVoidMethod(godot_instance, _finish); } } @@ -183,8 +177,7 @@ void GodotJavaWrapper::force_quit(JNIEnv *p_env) { void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) { if (_set_keep_screen_on) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + ERR_FAIL_NULL(env); env->CallVoidMethod(godot_instance, _set_keep_screen_on, p_enabled); } } @@ -192,8 +185,7 @@ void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) { void GodotJavaWrapper::alert(const String &p_message, const String &p_title) { if (_alert) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + ERR_FAIL_NULL(env); jstring jStrMessage = env->NewStringUTF(p_message.utf8().get_data()); jstring jStrTitle = env->NewStringUTF(p_title.utf8().get_data()); env->CallVoidMethod(godot_instance, _alert, jStrMessage, jStrTitle); @@ -202,24 +194,21 @@ void GodotJavaWrapper::alert(const String &p_message, const String &p_title) { int GodotJavaWrapper::get_gles_version_code() { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 0); - + ERR_FAIL_NULL_V(env, 0); if (_get_GLES_version_code) { return env->CallIntMethod(godot_instance, _get_GLES_version_code); } - return 0; } bool GodotJavaWrapper::has_get_clipboard() { - return _get_clipboard != 0; + return _get_clipboard != nullptr; } String GodotJavaWrapper::get_clipboard() { if (_get_clipboard) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); - + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_clipboard); return jstring_to_string(s, env); } else { @@ -230,8 +219,7 @@ String GodotJavaWrapper::get_clipboard() { String GodotJavaWrapper::get_input_fallback_mapping() { if (_get_input_fallback_mapping) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); - + ERR_FAIL_NULL_V(env, String()); jstring fallback_mapping = (jstring)env->CallObjectMethod(godot_instance, _get_input_fallback_mapping); return jstring_to_string(fallback_mapping, env); } else { @@ -240,28 +228,26 @@ String GodotJavaWrapper::get_input_fallback_mapping() { } bool GodotJavaWrapper::has_set_clipboard() { - return _set_clipboard != 0; + return _set_clipboard != nullptr; } void GodotJavaWrapper::set_clipboard(const String &p_text) { if (_set_clipboard) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + ERR_FAIL_NULL(env); jstring jStr = env->NewStringUTF(p_text.utf8().get_data()); env->CallVoidMethod(godot_instance, _set_clipboard, jStr); } } bool GodotJavaWrapper::has_has_clipboard() { - return _has_clipboard != 0; + return _has_clipboard != nullptr; } bool GodotJavaWrapper::has_clipboard() { if (_has_clipboard) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); - + ERR_FAIL_NULL_V(env, false); return env->CallBooleanMethod(godot_instance, _has_clipboard); } else { return false; @@ -271,8 +257,7 @@ bool GodotJavaWrapper::has_clipboard() { bool GodotJavaWrapper::request_permission(const String &p_name) { if (_request_permission) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); - + ERR_FAIL_NULL_V(env, false); jstring jStrName = env->NewStringUTF(p_name.utf8().get_data()); return env->CallBooleanMethod(godot_instance, _request_permission, jStrName); } else { @@ -283,8 +268,7 @@ bool GodotJavaWrapper::request_permission(const String &p_name) { bool GodotJavaWrapper::request_permissions() { if (_request_permissions) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); - + ERR_FAIL_NULL_V(env, false); return env->CallBooleanMethod(godot_instance, _request_permissions); } else { return false; @@ -295,14 +279,12 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const { Vector<String> permissions_list; if (_get_granted_permissions) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, permissions_list); - + ERR_FAIL_NULL_V(env, permissions_list); jobject permissions_object = env->CallObjectMethod(godot_instance, _get_granted_permissions); jobjectArray *arr = reinterpret_cast<jobjectArray *>(&permissions_object); - int i = 0; jsize len = env->GetArrayLength(*arr); - for (i = 0; i < len; i++) { + for (int i = 0; i < len; i++) { jstring jstr = (jstring)env->GetObjectArrayElement(*arr, i); String str = jstring_to_string(jstr, env); permissions_list.push_back(str); @@ -315,8 +297,7 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const { void GodotJavaWrapper::init_input_devices() { if (_init_input_devices) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + ERR_FAIL_NULL(env); env->CallVoidMethod(godot_instance, _init_input_devices); } } @@ -324,8 +305,7 @@ void GodotJavaWrapper::init_input_devices() { jobject GodotJavaWrapper::get_surface() { if (_get_surface) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, nullptr); - + ERR_FAIL_NULL_V(env, nullptr); return env->CallObjectMethod(godot_instance, _get_surface); } else { return nullptr; @@ -335,8 +315,7 @@ jobject GodotJavaWrapper::get_surface() { bool GodotJavaWrapper::is_activity_resumed() { if (_is_activity_resumed) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); - + ERR_FAIL_NULL_V(env, false); return env->CallBooleanMethod(godot_instance, _is_activity_resumed); } else { return false; @@ -346,8 +325,19 @@ bool GodotJavaWrapper::is_activity_resumed() { void GodotJavaWrapper::vibrate(int p_duration_ms) { if (_vibrate) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + ERR_FAIL_NULL(env); 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_NULL(env); + 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..bbf7c0ae33 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" @@ -50,26 +51,27 @@ private: GodotJavaViewWrapper *_godot_view = nullptr; - jmethodID _on_video_init = 0; - jmethodID _restart = 0; - jmethodID _finish = 0; - jmethodID _set_keep_screen_on = 0; - jmethodID _alert = 0; - jmethodID _get_GLES_version_code = 0; - jmethodID _get_clipboard = 0; - jmethodID _set_clipboard = 0; - jmethodID _has_clipboard = 0; - jmethodID _request_permission = 0; - jmethodID _request_permissions = 0; - jmethodID _get_granted_permissions = 0; - jmethodID _init_input_devices = 0; - jmethodID _get_surface = 0; - jmethodID _is_activity_resumed = 0; - jmethodID _vibrate = 0; - jmethodID _get_input_fallback_mapping = 0; - jmethodID _on_godot_setup_completed = 0; - jmethodID _on_godot_main_loop_started = 0; - jmethodID _get_class_loader = 0; + jmethodID _on_video_init = nullptr; + jmethodID _restart = nullptr; + jmethodID _finish = nullptr; + jmethodID _set_keep_screen_on = nullptr; + jmethodID _alert = nullptr; + jmethodID _get_GLES_version_code = nullptr; + jmethodID _get_clipboard = nullptr; + jmethodID _set_clipboard = nullptr; + jmethodID _has_clipboard = nullptr; + jmethodID _request_permission = nullptr; + jmethodID _request_permissions = nullptr; + jmethodID _get_granted_permissions = nullptr; + jmethodID _init_input_devices = nullptr; + jmethodID _get_surface = nullptr; + jmethodID _is_activity_resumed = nullptr; + jmethodID _vibrate = nullptr; + jmethodID _get_input_fallback_mapping = nullptr; + jmethodID _on_godot_setup_completed = nullptr; + jmethodID _on_godot_main_loop_started = nullptr; + jmethodID _get_class_loader = nullptr; + jmethodID _create_new_godot_instance = nullptr; 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 */ +#endif // JAVA_GODOT_WRAPPER_H diff --git a/platform/android/jni_utils.cpp b/platform/android/jni_utils.cpp index e2573d10f8..193ef61264 100644 --- a/platform/android/jni_utils.cpp +++ b/platform/android/jni_utils.cpp @@ -123,10 +123,10 @@ jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_a for (int j = 0; j < keys.size(); j++) { Variant var = dict[keys[j]]; - jvalret v = _variant_to_jvalue(env, var.get_type(), &var, true); - env->SetObjectArrayElement(jvalues, j, v.val.l); - if (v.obj) { - env->DeleteLocalRef(v.obj); + jvalret valret = _variant_to_jvalue(env, var.get_type(), &var, true); + env->SetObjectArrayElement(jvalues, j, valret.val.l); + if (valret.obj) { + env->DeleteLocalRef(valret.obj); } } @@ -186,7 +186,7 @@ String _get_class_name(JNIEnv *env, jclass cls, bool *array) { if (array) { jmethodID isArray = env->GetMethodID(cclass, "isArray", "()Z"); jboolean isarr = env->CallBooleanMethod(cls, isArray); - (*array) = isarr ? true : false; + (*array) = isarr != 0; } String name = jstring_to_string(clsName, env); env->DeleteLocalRef(clsName); diff --git a/platform/android/net_socket_android.cpp b/platform/android/net_socket_android.cpp index a65e7c6724..225a1132fe 100644 --- a/platform/android/net_socket_android.cpp +++ b/platform/android/net_socket_android.cpp @@ -32,10 +32,10 @@ #include "thread_jandroid.h" -jobject NetSocketAndroid::net_utils = 0; -jclass NetSocketAndroid::cls = 0; -jmethodID NetSocketAndroid::_multicast_lock_acquire = 0; -jmethodID NetSocketAndroid::_multicast_lock_release = 0; +jobject NetSocketAndroid::net_utils = nullptr; +jclass NetSocketAndroid::cls = nullptr; +jmethodID NetSocketAndroid::_multicast_lock_acquire = nullptr; +jmethodID NetSocketAndroid::_multicast_lock_release = nullptr; void NetSocketAndroid::setup(jobject p_net_utils) { JNIEnv *env = get_jni_env(); diff --git a/platform/android/net_socket_android.h b/platform/android/net_socket_android.h index 3e919497ea..97a611cb04 100644 --- a/platform/android/net_socket_android.h +++ b/platform/android/net_socket_android.h @@ -74,4 +74,4 @@ public: ~NetSocketAndroid(); }; -#endif +#endif // NET_SOCKET_ANDROID_H diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index b17b0f3139..0f551e7f4f 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -35,9 +35,12 @@ #include "drivers/unix/file_access_unix.h" #include "main/main.h" #include "platform/android/display_server_android.h" +#include "scene/main/scene_tree.h" +#include "servers/rendering_server.h" #include "dir_access_jandroid.h" #include "file_access_android.h" +#include "file_access_filesystem_jandroid.h" #include "net_socket_android.h" #include <dlfcn.h> @@ -45,6 +48,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. @@ -72,29 +77,36 @@ public: }; void OS_Android::alert(const String &p_alert, const String &p_title) { - GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); - ERR_FAIL_COND(!godot_java); - + ERR_FAIL_NULL(godot_java); godot_java->alert(p_alert, 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); + FileAccess::make_default<FileAccessFilesystemJAndroid>(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); + DirAccess::make_default<DirAccessJAndroid>(DirAccess::ACCESS_FILESYSTEM); NetSocketAndroid::make_default(); } @@ -125,7 +137,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() { @@ -148,9 +160,14 @@ Vector<String> OS_Android::get_granted_permissions() const { return godot_java->get_granted_permissions(); } -Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { p_library_handle = dlopen(p_path.utf8().get_data(), RTLD_NOW); - ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + "."); + ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + "."); + + if (r_resolved_path != nullptr) { + *r_resolved_path = p_path; + } + return OK; } @@ -168,16 +185,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 +225,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 +254,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; @@ -261,6 +301,33 @@ String OS_Android::get_system_dir(SystemDir p_dir, bool p_shared_storage) const return godot_io_java->get_system_dir(p_dir, p_shared_storage); } +Error OS_Android::move_to_trash(const String &p_path) { + Ref<DirAccess> da_ref = DirAccess::create_for_path(p_path); + if (da_ref.is_null()) { + return FAILED; + } + + // Check if it's a directory + if (da_ref->dir_exists(p_path)) { + Error err = da_ref->change_dir(p_path); + if (err) { + return err; + } + // This is directory, let's erase its contents + err = da_ref->erase_contents_recursive(); + if (err) { + return err; + } + // Remove the top directory + return da_ref->remove(p_path); + } else if (da_ref->file_exists(p_path)) { + // This is a file, let's remove it. + return da_ref->remove(p_path); + } else { + return FAILED; + } +} + void OS_Android::set_display_size(const Size2i &p_size) { display_size = p_size; } @@ -271,7 +338,7 @@ Size2i OS_Android::get_display_size() const { void OS_Android::set_opengl_extensions(const char *p_gl_extensions) { #if defined(GLES3_ENABLED) - ERR_FAIL_COND(!p_gl_extensions); + ERR_FAIL_NULL(p_gl_extensions); gl_extensions = p_gl_extensions; #endif } @@ -294,6 +361,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 +414,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..96c06d715c 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; @@ -88,13 +90,13 @@ public: virtual void alert(const String &p_alert, const String &p_title) override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual String get_name() const override; 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; @@ -119,11 +122,19 @@ public: virtual String get_system_dir(SystemDir p_dir, bool p_shared_storage = true) const override; + virtual Error move_to_trash(const String &p_path) override; + 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(); }; -#endif +#endif // OS_ANDROID_H diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp index 48aeb3d070..7a39e2003d 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -114,19 +114,18 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitS String signal_name = jstring_to_string(j_signal_name, env); int count = env->GetArrayLength(j_signal_params); - ERR_FAIL_COND_MSG(count > VARIANT_ARG_MAX, "Maximum argument count exceeded!"); - Variant variant_params[VARIANT_ARG_MAX]; - const Variant *args[VARIANT_ARG_MAX]; + Variant *variant_params = (Variant *)alloca(sizeof(Variant) * count); + const Variant **args = (const Variant **)alloca(sizeof(Variant *) * count); for (int i = 0; i < count; i++) { jobject j_param = env->GetObjectArrayElement(j_signal_params, i); variant_params[i] = _jobject_to_variant(env, j_param); args[i] = &variant_params[i]; env->DeleteLocalRef(j_param); - }; + } - singleton->emit_signal(StringName(signal_name), args, count); + singleton->emit_signalp(StringName(signal_name), args, count); } JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDNativeLibraries(JNIEnv *env, jclass clazz, jobjectArray gdnlib_paths) { diff --git a/platform/android/string_android.h b/platform/android/string_android.h index 2afaa86fec..79c71b5d04 100644 --- a/platform/android/string_android.h +++ b/platform/android/string_android.h @@ -30,6 +30,7 @@ #ifndef STRING_ANDROID_H #define STRING_ANDROID_H + #include "core/string/ustring.h" #include "thread_jandroid.h" #include <jni.h> diff --git a/platform/android/thread_jandroid.h b/platform/android/thread_jandroid.h index 2073423f8d..3b000517fd 100644 --- a/platform/android/thread_jandroid.h +++ b/platform/android/thread_jandroid.h @@ -38,4 +38,4 @@ void init_thread_jandroid(JavaVM *p_jvm, JNIEnv *p_env); void setup_android_thread(); JNIEnv *get_jni_env(); -#endif +#endif // THREAD_JANDROID_H diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp new file mode 100644 index 0000000000..27ba8da448 --- /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 = nullptr; +jclass TTS_Android::cls = nullptr; + +jmethodID TTS_Android::_is_speaking = nullptr; +jmethodID TTS_Android::_is_paused = nullptr; +jmethodID TTS_Android::_get_voices = nullptr; +jmethodID TTS_Android::_speak = nullptr; +jmethodID TTS_Android::_pause_speaking = nullptr; +jmethodID TTS_Android::_resume_speaking = nullptr; +jmethodID TTS_Android::_stop_speaking = nullptr; + +HashMap<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 (const KeyValue<int, Char16String> &E : ids) { + 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..bc0cdb8d55 --- /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 HashMap<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 |