From a24429c192cfee5ae92697f12f219cad27635fb4 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Tue, 9 Jun 2026 00:52:31 -0400 Subject: [PATCH] harmonyos: Port SDL3 to HarmonyOS/OpenHarmony. This is my port of SDL3 to HarmonyOS/OpenHarmony. It has been tested on real hardware from Huawei, and commercial games are being prepped to ship with it already. There is _heavy_ documentation in docs/README-harmonyos.md, including how to set up a development environment and project, build, and debug, for the completely uninitiated. This work was sponsored by Outfit7: https://outfit7.com/ Fixes #9837. --- .gitignore | 10 + CMakeLists.txt | 169 ++- build-scripts/harmonyos-tool.sh | 209 +++ cmake/PreseedOpenHarmonyCache.cmake | 188 +++ cmake/macros.cmake | 2 +- cmake/sdlcompilers.cmake | 5 + docs/README-harmonyos.md | 872 ++++++++++++ docs/README-platforms.md | 1 + include/SDL3/SDL_iostream.h | 13 +- include/SDL3/SDL_keyboard.h | 7 + include/SDL3/SDL_main.h | 13 + include/SDL3/SDL_main_impl.h | 7 + include/SDL3/SDL_platform_defines.h | 11 + include/SDL3/SDL_system.h | 95 +- include/SDL3/SDL_video.h | 12 + include/build_config/SDL_build_config.h.cmake | 5 + openharmony-project/.gitignore | 12 + openharmony-project/AppScope/app.json5 | 11 + .../resources/base/element/string.json | 8 + .../resources/base/media/background.png | Bin 0 -> 91942 bytes .../resources/base/media/foreground.png | Bin 0 -> 15325 bytes .../resources/base/media/layered_image.json | 7 + openharmony-project/build-profile.json5 | 42 + openharmony-project/code-linter.json5 | 32 + openharmony-project/entry/.gitignore | 6 + openharmony-project/entry/build-profile.json5 | 45 + openharmony-project/entry/hvigorfile.ts | 6 + .../entry/obfuscation-rules.txt | 23 + .../entry/oh-package-lock.json5 | 19 + openharmony-project/entry/oh-package.json5 | 11 + .../entry/src/main/cpp/CMakeLists.txt | 18 + .../src/main/cpp/types/libSDL3/Index.d.ts | 1 + .../main/cpp/types/libSDL3/oh-package.json5 | 7 + .../main/ets/entryability/EntryAbility.ets | 28 + .../entrybackupability/EntryBackupAbility.ets | 16 + .../entry/src/main/ets/pages/Index.ets | 15 + .../entry/src/main/module.json5 | 67 + .../main/resources/base/element/color.json | 8 + .../main/resources/base/element/float.json | 8 + .../main/resources/base/element/string.json | 33 + .../main/resources/base/media/background.png | Bin 0 -> 91942 bytes .../main/resources/base/media/foreground.png | Bin 0 -> 8805 bytes .../resources/base/media/layered_image.json | 7 + .../main/resources/base/media/startIcon.png | Bin 0 -> 20093 bytes .../resources/base/profile/backup_config.json | 3 + .../resources/base/profile/main_pages.json | 5 + .../main/resources/dark/element/color.json | 8 + .../hvigor/hvigor-config.json5 | 23 + openharmony-project/hvigorfile.ts | 6 + openharmony-project/oh-package-lock.json5 | 28 + openharmony-project/oh-package.json5 | 10 + src/SDL.c | 7 + src/SDL_log.c | 29 +- src/audio/SDL_audio.c | 3 + src/audio/SDL_sysaudio.h | 1 + src/audio/ohaudio/SDL_ohaudio.c | 648 +++++++++ src/audio/ohaudio/SDL_ohaudio.h | 38 + src/audio/ohaudio/SDL_ohaudiofuncs.h | 62 + src/camera/SDL_camera.c | 3 + src/camera/SDL_syscamera.h | 1 + .../openharmony/SDL_camera_openharmony.c | 775 +++++++++++ src/core/SDL_core_unsupported.c | 22 + src/core/SDL_core_unsupported.h | 8 + src/core/android/SDL_android.h | 4 + src/core/openharmony/SDL_openharmony.c | 1207 +++++++++++++++++ src/core/openharmony/SDL_openharmony.h | 66 + src/cpuinfo/SDL_cpuinfo.c | 2 +- src/dynapi/SDL_dynapi.exports | 3 + src/dynapi/SDL_dynapi.h | 2 + src/dynapi/SDL_dynapi.sym | 3 + src/dynapi/SDL_dynapi_overrides.h | 3 + src/dynapi/SDL_dynapi_procs.h | 3 + src/events/SDL_keyboard.c | 6 +- src/filesystem/SDL_filesystem.c | 2 +- .../openharmony/SDL_sysfilesystem.c | 78 ++ src/filesystem/posix/SDL_sysfsops.c | 36 +- src/io/SDL_iostream.c | 38 +- src/locale/openharmony/SDL_syslocale.c | 36 + src/locale/psp/SDL_syslocale.c | 1 - src/main/SDL_main_callbacks.c | 9 +- src/main/generic/SDL_sysmain_callbacks.c | 1 + src/main/openharmony/SDL_sysmain_callbacks.c | 60 + src/main/openharmony/SDL_sysmain_runapp.c | 102 ++ src/misc/android/SDL_sysurl.c | 5 + src/misc/openharmony/SDL_sysurl.c | 34 + src/power/SDL_power.c | 3 + src/power/SDL_syspower.h | 1 + src/power/openharmony/SDL_syspower.c | 46 + src/render/SDL_render.c | 2 +- src/thread/pthread/SDL_systhread.c | 16 +- src/video/SDL_egl.c | 4 + src/video/SDL_sysvideo.h | 1 + src/video/SDL_video.c | 3 + src/video/SDL_vulkan_internal.h | 3 + .../openharmony/SDL_openharmonyclipboard.c | 220 +++ .../openharmony/SDL_openharmonyclipboard.h | 34 + src/video/openharmony/SDL_openharmonyevents.c | 574 ++++++++ src/video/openharmony/SDL_openharmonyevents.h | 33 + .../openharmony/SDL_openharmonykeyboard.c | 93 ++ .../openharmony/SDL_openharmonykeyboard.h | 29 + src/video/openharmony/SDL_openharmonymouse.c | 76 ++ src/video/openharmony/SDL_openharmonymouse.h | 30 + src/video/openharmony/SDL_openharmonyopengl.c | 79 ++ src/video/openharmony/SDL_openharmonyopengl.h | 31 + src/video/openharmony/SDL_openharmonyvideo.c | 280 ++++ src/video/openharmony/SDL_openharmonyvideo.h | 47 + src/video/openharmony/SDL_openharmonyvulkan.c | 174 +++ src/video/openharmony/SDL_openharmonyvulkan.h | 52 + src/video/openharmony/SDL_openharmonywindow.c | 187 +++ src/video/openharmony/SDL_openharmonywindow.h | 51 + test/CMakeLists.txt | 4 +- test/testclipboard.c | 6 +- test/testsymbols.c | 6 + test/testurl.c | 4 +- test/testvulkan.c | 90 +- 115 files changed, 7518 insertions(+), 91 deletions(-) create mode 100755 build-scripts/harmonyos-tool.sh create mode 100644 cmake/PreseedOpenHarmonyCache.cmake create mode 100644 docs/README-harmonyos.md create mode 100644 openharmony-project/.gitignore create mode 100644 openharmony-project/AppScope/app.json5 create mode 100644 openharmony-project/AppScope/resources/base/element/string.json create mode 100644 openharmony-project/AppScope/resources/base/media/background.png create mode 100644 openharmony-project/AppScope/resources/base/media/foreground.png create mode 100644 openharmony-project/AppScope/resources/base/media/layered_image.json create mode 100644 openharmony-project/build-profile.json5 create mode 100644 openharmony-project/code-linter.json5 create mode 100644 openharmony-project/entry/.gitignore create mode 100644 openharmony-project/entry/build-profile.json5 create mode 100644 openharmony-project/entry/hvigorfile.ts create mode 100644 openharmony-project/entry/obfuscation-rules.txt create mode 100644 openharmony-project/entry/oh-package-lock.json5 create mode 100644 openharmony-project/entry/oh-package.json5 create mode 100644 openharmony-project/entry/src/main/cpp/CMakeLists.txt create mode 100644 openharmony-project/entry/src/main/cpp/types/libSDL3/Index.d.ts create mode 100644 openharmony-project/entry/src/main/cpp/types/libSDL3/oh-package.json5 create mode 100644 openharmony-project/entry/src/main/ets/entryability/EntryAbility.ets create mode 100644 openharmony-project/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets create mode 100644 openharmony-project/entry/src/main/ets/pages/Index.ets create mode 100644 openharmony-project/entry/src/main/module.json5 create mode 100644 openharmony-project/entry/src/main/resources/base/element/color.json create mode 100644 openharmony-project/entry/src/main/resources/base/element/float.json create mode 100644 openharmony-project/entry/src/main/resources/base/element/string.json create mode 100644 openharmony-project/entry/src/main/resources/base/media/background.png create mode 100644 openharmony-project/entry/src/main/resources/base/media/foreground.png create mode 100644 openharmony-project/entry/src/main/resources/base/media/layered_image.json create mode 100644 openharmony-project/entry/src/main/resources/base/media/startIcon.png create mode 100644 openharmony-project/entry/src/main/resources/base/profile/backup_config.json create mode 100644 openharmony-project/entry/src/main/resources/base/profile/main_pages.json create mode 100644 openharmony-project/entry/src/main/resources/dark/element/color.json create mode 100644 openharmony-project/hvigor/hvigor-config.json5 create mode 100644 openharmony-project/hvigorfile.ts create mode 100644 openharmony-project/oh-package-lock.json5 create mode 100644 openharmony-project/oh-package.json5 create mode 100644 src/audio/ohaudio/SDL_ohaudio.c create mode 100644 src/audio/ohaudio/SDL_ohaudio.h create mode 100644 src/audio/ohaudio/SDL_ohaudiofuncs.h create mode 100644 src/camera/openharmony/SDL_camera_openharmony.c create mode 100644 src/core/openharmony/SDL_openharmony.c create mode 100644 src/core/openharmony/SDL_openharmony.h create mode 100644 src/filesystem/openharmony/SDL_sysfilesystem.c create mode 100644 src/locale/openharmony/SDL_syslocale.c create mode 100644 src/main/openharmony/SDL_sysmain_callbacks.c create mode 100644 src/main/openharmony/SDL_sysmain_runapp.c create mode 100644 src/misc/openharmony/SDL_sysurl.c create mode 100644 src/power/openharmony/SDL_syspower.c create mode 100644 src/video/openharmony/SDL_openharmonyclipboard.c create mode 100644 src/video/openharmony/SDL_openharmonyclipboard.h create mode 100644 src/video/openharmony/SDL_openharmonyevents.c create mode 100644 src/video/openharmony/SDL_openharmonyevents.h create mode 100644 src/video/openharmony/SDL_openharmonykeyboard.c create mode 100644 src/video/openharmony/SDL_openharmonykeyboard.h create mode 100644 src/video/openharmony/SDL_openharmonymouse.c create mode 100644 src/video/openharmony/SDL_openharmonymouse.h create mode 100644 src/video/openharmony/SDL_openharmonyopengl.c create mode 100644 src/video/openharmony/SDL_openharmonyopengl.h create mode 100644 src/video/openharmony/SDL_openharmonyvideo.c create mode 100644 src/video/openharmony/SDL_openharmonyvideo.h create mode 100644 src/video/openharmony/SDL_openharmonyvulkan.c create mode 100644 src/video/openharmony/SDL_openharmonyvulkan.h create mode 100644 src/video/openharmony/SDL_openharmonywindow.c create mode 100644 src/video/openharmony/SDL_openharmonywindow.h diff --git a/.gitignore b/.gitignore index 6ae2fd4100..b419000db2 100644 --- a/.gitignore +++ b/.gitignore @@ -99,6 +99,16 @@ out/ android-project/local.properties android-project/.gradle/ +# for OpenHarmony/HarmonyOS +openharmony-project/entry/src/main/cpp/CMakeLists.txt +openharmony-project/entry/src/main/cpp/*.c +openharmony-project/entry/src/main/cpp/*.h +openharmony-project/entry/src/main/cpp/SDL +openharmony-project/entry/src/main/resources/rawfile +openharmony-project/AppScope/app.json5 +openharmony-project/AppScope/resources/base/element/string.json +openharmony-project/build-profile.json5 + test/checkkeys test/checkkeysthreads test/gamepadmap diff --git a/CMakeLists.txt b/CMakeLists.txt index 4cabec1374..d0095516e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ include("${SDL3_SOURCE_DIR}/cmake/GetGitRevisionDescription.cmake") include("${SDL3_SOURCE_DIR}/cmake/3rdparty.cmake") include("${SDL3_SOURCE_DIR}/cmake/PreseedMSVCCache.cmake") include("${SDL3_SOURCE_DIR}/cmake/PreseedEmscriptenCache.cmake") +include("${SDL3_SOURCE_DIR}/cmake/PreseedOpenHarmonyCache.cmake") include("${SDL3_SOURCE_DIR}/cmake/PreseedHaikuCache.cmake") include("${SDL3_SOURCE_DIR}/cmake/PreseedNokiaNGageCache.cmake") include("${SDL3_SOURCE_DIR}/cmake/PreseedDOSCache.cmake") @@ -322,7 +323,7 @@ dep_option(SDL_SSE4_2 "Use SSE4.2 assembly routines" ON "SDL_ASSEMB dep_option(SDL_MMX "Use MMX assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_X86 OR SDL_CPU_X64" OFF) dep_option(SDL_ALTIVEC "Use Altivec assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_POWERPC32 OR SDL_CPU_POWERPC64" OFF) dep_option(SDL_ARMNEON "Use NEON assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_ARM32 OR SDL_CPU_ARM64" OFF) -dep_option(SDL_ARMSVE2 "Use SVE2 assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_ARM64" OFF) +dep_option(SDL_ARMSVE2 "Use SVE2 assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_ARM64 AND NOT OHOS" OFF) dep_option(SDL_LSX "Use LSX assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_LOONGARCH64" OFF) dep_option(SDL_LASX "Use LASX assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_LOONGARCH64" OFF) @@ -385,7 +386,7 @@ dep_option(SDL_RENDER_D3D12 "Enable the Direct3D 12 render driver" ON "SD dep_option(SDL_RENDER_METAL "Enable the Metal render driver" ON "SDL_RENDER;APPLE" OFF) dep_option(SDL_RENDER_GPU "Enable the SDL_GPU render driver" ON "SDL_RENDER;SDL_GPU" OFF) dep_option(SDL_VIVANTE "Use Vivante EGL video driver" ON "${UNIX_SYS};SDL_CPU_ARM32" OFF) -dep_option(SDL_VULKAN "Enable Vulkan support" "${SDL_VULKAN_DEFAULT}" "SDL_VIDEO;ANDROID OR APPLE OR LINUX OR FREEBSD OR OPENBSD OR WINDOWS OR CYGWIN" OFF) +dep_option(SDL_VULKAN "Enable Vulkan support" "${SDL_VULKAN_DEFAULT}" "SDL_VIDEO;ANDROID OR APPLE OR LINUX OR FREEBSD OR OPENBSD OR WINDOWS OR CYGWIN OR OHOS" OFF) dep_option(SDL_RENDER_VULKAN "Enable the Vulkan render driver" ON "SDL_RENDER;SDL_VULKAN" OFF) dep_option(SDL_METAL "Enable Metal support" ON "APPLE" OFF) set_option(SDL_OPENVR "Use OpenVR video driver" OFF) @@ -1724,6 +1725,170 @@ if(ANDROID) target_link_options(SDL3-static INTERFACE "-Wl,-u,JNI_OnLoad") endif() +elseif(OHOS) # OpenHarmony/HarmonyOS + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/core/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/core/openharmony/*.h" + ) + + sdl_link_dependency(openharmonycore LIBS libace_napi.z.so libace_ndk.z.so deviceinfo_ndk.z rawfile.z hilog_ndk.z ohcommonevent) + + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/main/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/main/openharmony/*.h" + ) + set(HAVE_SDL_MAIN_CALLBACKS TRUE) + + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/misc/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/misc/openharmony/*.h" + ) + set(HAVE_SDL_MISC TRUE) + + if(SDL_AUDIO) + set(SDL_AUDIO_DRIVER_OHAUDIO 1) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/audio/ohaudio/*.c" + "${SDL3_SOURCE_DIR}/src/audio/ohaudio/*.h" + ) + + set(HAVE_SDL_AUDIO TRUE) + endif() + + set(SDL_FILESYSTEM_OPENHARMONY 1) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/filesystem/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/filesystem/openharmony/*.h" + ) + sdl_link_dependency(openharmonyfs LIBS ability_runtime) + set(HAVE_SDL_FILESYSTEM TRUE) + + set(SDL_FSOPS_POSIX 1) + sdl_sources("${SDL3_SOURCE_DIR}/src/filesystem/posix/SDL_sysfsops.c") + set(HAVE_SDL_FSOPS TRUE) + +if (False) # !!! FIXME + if(SDL_HAPTIC) + set(SDL_HAPTIC_OPENHARMONY 1) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/haptic/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/haptic/openharmony/*.h" + ) + set(HAVE_SDL_HAPTIC TRUE) + endif() + + # !!! FIXME: any chance HIDAPI would work here? + CheckHIDAPI() + + if(SDL_JOYSTICK) + set(SDL_JOYSTICK_OPENHARMONY 1) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/joystick/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/joystick/openharmony/*.h" + ) + set(HAVE_SDL_JOYSTICK TRUE) + endif() +endif() + + set(SDL_LOADSO_DLOPEN 1) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/loadso/dlopen/*.c" + "${SDL3_SOURCE_DIR}/src/loadso/dlopen/*.h" + ) + set(HAVE_SDL_LOADSO TRUE) + + if(SDL_POWER) + set(SDL_POWER_OPENHARMONY 1) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/power/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/power/openharmony/*.h" + ) + sdl_link_dependency(openharmonypower LIBS ohbattery_info) + set(HAVE_SDL_POWER TRUE) + endif() + + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/locale/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/locale/openharmony/*.h" + ) + set(HAVE_SDL_LOCALE TRUE) + + set(SDL_TIME_UNIX 1) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/time/unix/*.c" + "${SDL3_SOURCE_DIR}/src/time/unix/*.h" + ) + set(HAVE_SDL_TIME TRUE) + + set(SDL_TIMER_UNIX 1) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/timer/unix/*.c" + "${SDL3_SOURCE_DIR}/src/timer/unix/*.h" + ) + set(HAVE_SDL_TIMERS TRUE) + +if(False) + if(SDL_SENSOR) + set(SDL_SENSOR_OPENHARMONY 1) + set(HAVE_SDL_SENSORS TRUE) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/sensor/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/sensor/openharmony/*.h" + ) + endif() +endif() + + if(SDL_CAMERA) + set(SDL_CAMERA_DRIVER_OPENHARMONY 1) + set(HAVE_CAMERA TRUE) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/camera/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/camera/openharmony/*.h" + ) + sdl_link_dependency(openharmonycamera LIBS ohcamera ohimage image_receiver native_image native_buffer) + endif() + + if(SDL_VIDEO) + set(SDL_VIDEO_DRIVER_OPENHARMONY 1) + sdl_glob_sources( + "${SDL3_SOURCE_DIR}/src/video/openharmony/*.c" + "${SDL3_SOURCE_DIR}/src/video/openharmony/*.h" + ) + set(HAVE_SDL_VIDEO TRUE) + sdl_link_dependency(openharmonyvideo LIBS native_display_manager pasteboard udmf) + + # Core stuff + # find_library(ANDROID_DL_LIBRARY dl) + # FIXME failing dlopen https://github.com/android-ndk/ndk/issues/929 +# !!! FIXME +# sdl_link_dependency(android_video LIBS dl log android) + sdl_compile_definitions(PRIVATE "GL_GLEXT_PROTOTYPES") + + #enable gles + if(SDL_OPENGLES) + set(SDL_VIDEO_OPENGL_EGL 1) + set(HAVE_OPENGLES TRUE) + set(SDL_VIDEO_OPENGL_ES2 1) + set(SDL_VIDEO_RENDER_OGL_ES2 1) + #sdl_link_dependency(opengles LIBS GLESv3) + endif() + + if(SDL_VULKAN) + set(SDL_VIDEO_VULKAN 1) + set(HAVE_VULKAN TRUE) + if(SDL_RENDER_VULKAN) + set(SDL_VIDEO_RENDER_VULKAN 1) + set(HAVE_RENDER_VULKAN TRUE) + endif() + endif() + endif() + + CheckPTHREAD() + + if(SDL_CLOCK_GETTIME) + set(HAVE_CLOCK_GETTIME 1) + endif() + elseif(EMSCRIPTEN) # Hide noisy warnings that intend to aid mostly during initial stages of porting a new # project. Uncomment at will for verbose cross-compiling -I/../ path info. diff --git a/build-scripts/harmonyos-tool.sh b/build-scripts/harmonyos-tool.sh new file mode 100755 index 0000000000..684b7241db --- /dev/null +++ b/build-scripts/harmonyos-tool.sh @@ -0,0 +1,209 @@ +#!/usr/bin/bash + +HVIGORWPATH=`which hvigorw 2>/dev/null` +if [ "x$HVIGORWPATH" == "x" ]; then + echo "Couldn't find hvigorw!" + echo "Please make sure the HarmonyOS command line tools are in your PATH." 1>&2 + exit 1 +fi + +HDCPATH=`which hdc 2>/dev/null` +if [ "x$HDCPATH" == "x" ]; then + echo "Couldn't find hdc!" + echo "Please make sure the HarmonyOS command line tools are in your PATH." 1>&2 + exit 1 +fi + +CLTBINPATH=`dirname "$HVIGORWPATH"` +CLTPATH=`dirname "$CLTBINPATH"` +CLT=`realpath "$CLTPATH"` +#echo "CLT is '$CLT'" + +need_usage=0 + +APPJSON="" +PROJDIR="$1" +if [ "x$PROJDIR" == "x" ]; then + need_usage=1 +else + PROJDIR=`realpath "$PROJDIR"` + APPJSON="$PROJDIR/AppScope/app.json5" + if [ ! -f "$APPJSON" ]; then + echo "Can't find '$APPJSON'. Is project directory correct?" 1>&2 + echo "" 1>&2 + need_usage=1 + fi +fi + +do_clean=0 +do_build=0 +do_uninstall=0 +do_install=0 +do_kill=0 +do_launch=0 +do_log=0 +do_debug=0 +do_anything=0 + +shift # dump projectdir +while [ $# -gt 0 ]; do + do_anything=1 + case $1 in + --clean) + do_clean=1 + shift + ;; + --build) + do_build=1 + shift + ;; + --install) + do_install=1 + shift + ;; + --uninstall) + do_uninstall=1 + shift + ;; + --kill) + do_kill=1 + shift + ;; + --launch) + do_launch=1 + shift + ;; + --log) + do_log=1 + shift + ;; + --debug) + do_launch=1 + do_debug=1 + shift + ;; + *) + echo "Unknown option '$1'" 1>&2 + need_usage=1 + shift + ;; + esac +done + +if [ "$do_anything" == "0" ]; then + need_usage=1 +fi + +if [ "$need_usage" == "1" ]; then + echo "USAGE: $0 [--kill] [--uninstall] [--build] [--install] [--launch] [--debug] [--log]" 1>&2 + echo "" 1>&2 + exit 1 +fi + +if [ "$do_build" == "1" ]; then + if [ "$do_debug" == "1" ]; then + do_install=1 + elif [ "$do_launch" == "1" ]; then + do_install=1 + fi +fi + +# Here we go! + +BUNDLE=`grep -F bundleName "$PROJDIR/AppScope/app.json5" |perl -w -p -e 's/\A\s*\"bundleName\"\s*\:\s*\"(.*?)\".*?\Z/$1/i;'` +if [ "x$BUNDLE" == "x" ]; then + echo "Failed to determine bundle name! Aborting!" 1>&2 + exit 1 +fi + +echo "BUNDLE is '$BUNDLE'" + +#set -x + +cd "$PROJDIR" + +if [ "$do_kill" == "1" ]; then + echo "KILLING..." + hdc shell aa force-stop $BUNDLE || exit 1 +fi + +if [ "$do_uninstall" == "1" ]; then + echo "UNINSTALLING..." + hdc uninstall $BUNDLE || exit 1 +fi + +if [ "$do_clean" == "1" ]; then + echo "CLEANING..." + hvigorw clean || exit 1 +fi + +if [ "$do_build" == "1" ]; then + echo "BUILDING..." + hvigorw assembleHap || exit 1 +fi + +if [ "$do_debug" == "1" ]; then + hdc shell mkdir -p data/local/tmp/debugserver/$BUNDLE + hdc file send "$CLT/sdk/default/hms/native/lldb/aarch64-linux-ohos/lldb-server" data/local/tmp/debugserver/$BUNDLE + hdc shell chmod 755 data/local/tmp/debugserver/$BUNDLE/lldb-server +fi + +if [ "$do_install" == "1" ]; then + echo "INSTALLING..." + hdc install "$PROJDIR/entry/build/default/outputs/default/entry-default-signed.hap" || exit 1 +fi + +# Catch sigint, kill the backgrounded `hdc hilog` then exit. +trap_sigint() { + if [ "$HILOGPID" != "" ]; then + kill -9 "$HILOGPID" + fi + wait "$HILOGPID" 2>/dev/null + echo "" + #echo "Killed $HILOGPID" + exit 0 +} + +HILOGPID="" +if [ "$do_log" == "1" ]; then + trap trap_sigint INT + + # This wild bash syntax puts `hdc` in the background, and pipes its output through `grep`, but `$!` will have hdc's process ID. + # The ridiculous perl regular expression basically filters out a ton of garbage from each line of the log. You can remove it entirely and just grep for the bundle name, though! + hdc hilog > >(grep -F --line-buffered "/$BUNDLE/" |perl -w -p -e 's/\A\d+\-\d+\s+(\d+\:\d+\:\d+)\.\d+\s+\d+\s+\d+\s+(.)\s+.*?\/.*?\/(.*?)\:\s+/$1 $2 $3: /;') & HILOGPID="$!" +fi + +if [ "$do_launch" == "1" ]; then + echo "LAUNCHING..." + # !!! FIXME: maybe this makes the process wait on launch until debugger is attached? + #hdc shell aa appdebug -b $BUNDLE + hdc shell aa start -a EntryAbility -b $BUNDLE || exit 1 +fi + +if [ "$do_debug" == "1" ]; then + echo "DEBUGGING..." + hdc shell aa attach -b $BUNDLE + hdc shell aa process -a EntryAbility -b $BUNDLE -D "/data/local/tmp/debugserver/$BUNDLE/lldb-server platform --listen unix-abstract:///lldb-server/platform.sock" + + PID=`hdc shell ps -w -o PID,ARGS |grep -F "$BUNDLE" |head -n 1 |awk '{print $1}'` + if [ "x$PID" == "x" ]; then + echo "Failed to determine PID! Aborting!" 1>&2 + exit 1 + fi + + echo "PID is $PID" + + trap - INT + "$CLT/sdk/default/openharmony/native/llvm/bin/lldb" \ + -O "platform select remote-ohos" \ + -O "platform connect unix-abstract-connect:///lldb-server/platform.sock" \ + -O "settings append target.exec-search-paths \"$PROJDIR/entry/build/default/intermediates/cmake/default/obj/arm64-v8a\"" \ + -O "attach $PID" + trap trap_sigint INT +fi + +if [ "$HILOGPID" != "" ]; then + #echo "$0: WAITING ON $HILOGPID" + wait "$HILOGPID" +fi + diff --git a/cmake/PreseedOpenHarmonyCache.cmake b/cmake/PreseedOpenHarmonyCache.cmake new file mode 100644 index 0000000000..b78c24aeb9 --- /dev/null +++ b/cmake/PreseedOpenHarmonyCache.cmake @@ -0,0 +1,188 @@ +if(OHOS) + function(SDL_Preseed_CMakeCache) + set(COMPILER_SUPPORTS_FDIAGNOSTICS_COLOR_ALWAYS "1" CACHE INTERNAL "Test COMPILER_SUPPORTS_FDIAGNOSTICS_COLOR_ALWAYS") + set(COMPILER_SUPPORTS_GCC_ATOMICS "1" CACHE INTERNAL "Test COMPILER_SUPPORTS_GCC_ATOMICS") + set(COMPILER_SUPPORTS_SYNC_LOCK_TEST_AND_SET "1" CACHE INTERNAL "Test COMPILER_SUPPORTS_SYNC_LOCK_TEST_AND_SET") + set(HAVE_CLANG_COMMENT_BLOCK_COMMANDS "1" CACHE INTERNAL "Test HAVE_CLANG_COMMENT_BLOCK_COMMANDS") + set(HAVE_ALLOCA_H "1" CACHE INTERNAL "Have include alloca.h") + set(HAVE_LIBM "1" CACHE INTERNAL "Have library m") + set(HAVE_POSIX_SPAWN "1" CACHE INTERNAL "Have symbol posix_spawn") + set(HAVE_FSEEKO "1" CACHE INTERNAL "Have symbol fseeko") + set(HAVE_OFF64_T "1" CACHE INTERNAL "Have symbol off64_t") + set(LIBC_HAS_ABS "1" CACHE INTERNAL "Have symbol abs") + set(LIBC_HAS_ACOS "1" CACHE INTERNAL "Have symbol acos") + set(LIBC_HAS_ACOSF "1" CACHE INTERNAL "Have symbol acosf") + set(LIBC_HAS_ASIN "1" CACHE INTERNAL "Have symbol asin") + set(LIBC_HAS_ASINF "1" CACHE INTERNAL "Have symbol asinf") + set(LIBC_HAS_ATAN "1" CACHE INTERNAL "Have symbol atan") + set(LIBC_HAS_ATAN2 "1" CACHE INTERNAL "Have symbol atan2") + set(LIBC_HAS_ATAN2F "1" CACHE INTERNAL "Have symbol atan2f") + set(LIBC_HAS_ATANF "1" CACHE INTERNAL "Have symbol atanf") + set(LIBC_HAS_ATOF "1" CACHE INTERNAL "Have symbol atof") + set(LIBC_HAS_ATOI "1" CACHE INTERNAL "Have symbol atoi") + set(LIBC_HAS_BCOPY "1" CACHE INTERNAL "Have symbol bcopy") + set(LIBC_HAS_CALLOC "1" CACHE INTERNAL "Have symbol calloc") + set(LIBC_HAS_CEIL "1" CACHE INTERNAL "Have symbol ceil") + set(LIBC_HAS_CEILF "1" CACHE INTERNAL "Have symbol ceilf") + set(LIBC_HAS_COPYSIGN "1" CACHE INTERNAL "Have symbol copysign") + set(LIBC_HAS_COPYSIGNF "1" CACHE INTERNAL "Have symbol copysignf") + set(LIBC_HAS_COS "1" CACHE INTERNAL "Have symbol cos") + set(LIBC_HAS_COSF "1" CACHE INTERNAL "Have symbol cosf") + set(LIBC_HAS_EXP "1" CACHE INTERNAL "Have symbol exp") + set(LIBC_HAS_EXPF "1" CACHE INTERNAL "Have symbol expf") + set(LIBC_HAS_FABS "1" CACHE INTERNAL "Have symbol fabs") + set(LIBC_HAS_FABSF "1" CACHE INTERNAL "Have symbol fabsf") + set(LIBC_HAS_FLOAT_H "1" CACHE INTERNAL "Have include float.h") + set(LIBC_HAS_FLOOR "1" CACHE INTERNAL "Have symbol floor") + set(LIBC_HAS_FLOORF "1" CACHE INTERNAL "Have symbol floorf") + set(LIBC_HAS_FMOD "1" CACHE INTERNAL "Have symbol fmod") + set(LIBC_HAS_FMODF "1" CACHE INTERNAL "Have symbol fmodf") + set(LIBC_HAS_FOPEN64 "1" CACHE INTERNAL "Have symbol fopen64") + set(LIBC_HAS_FREE "1" CACHE INTERNAL "Have symbol free") + set(LIBC_HAS_FSEEKO "1" CACHE INTERNAL "Have symbol fseeko") + set(LIBC_HAS_FSEEKO64 "1" CACHE INTERNAL "Have symbol fseeko64 (broken in DJGPP)") + set(LIBC_HAS_GETENV "1" CACHE INTERNAL "Have symbol getenv") + set(LIBC_HAS_ICONV_H "1" CACHE INTERNAL "Have include iconv.h") + set(LIBC_HAS_INDEX "1" CACHE INTERNAL "Have symbol index") + set(LIBC_HAS_INTTYPES_H "1" CACHE INTERNAL "Have include inttypes.h") + set(LIBC_HAS_ISINF "" CACHE INTERNAL "Have include isinf(double)") + set(LIBC_ISINF_HANDLES_FLOAT "" CACHE INTERNAL "Have include isinf(float)") + set(LIBC_HAS_ISINFF "" CACHE INTERNAL "Have include isinff(float)") + set(LIBC_HAS_ISNAN "" CACHE INTERNAL "Have include isnan(double)") + set(LIBC_ISNAN_HANDLES_FLOAT "" CACHE INTERNAL "Have include isnan(float)") + set(LIBC_HAS_ISNANF "" CACHE INTERNAL "Have include isnanf(float)") + set(LIBC_HAS_ITOA "" CACHE INTERNAL "Have symbol itoa") + set(LIBC_HAS_LIMITS_H "1" CACHE INTERNAL "Have include limits.h") + set(LIBC_HAS_LOG "1" CACHE INTERNAL "Have symbol log") + set(LIBC_HAS_LOG10 "1" CACHE INTERNAL "Have symbol log10") + set(LIBC_HAS_LOG10F "1" CACHE INTERNAL "Have symbol log10f") + set(LIBC_HAS_LOGF "1" CACHE INTERNAL "Have symbol logf") + set(LIBC_HAS_LROUND "1" CACHE INTERNAL "Have symbol lround") + set(LIBC_HAS_LROUNDF "1" CACHE INTERNAL "Have symbol lroundf") + set(LIBC_HAS_MALLOC "1" CACHE INTERNAL "Have symbol malloc") + set(LIBC_HAS_MALLOC_H "1" CACHE INTERNAL "Have include malloc.h") + set(LIBC_HAS_MATH_H "1" CACHE INTERNAL "Have include math.h") + set(LIBC_HAS_MEMCMP "1" CACHE INTERNAL "Have symbol memcmp") + set(LIBC_HAS_MEMCPY "1" CACHE INTERNAL "Have symbol memcpy") + set(LIBC_HAS_MEMMOVE "1" CACHE INTERNAL "Have symbol memmove") + set(LIBC_HAS_MEMORY_H "1" CACHE INTERNAL "Have include memory.h") + set(LIBC_HAS_MEMSET "1" CACHE INTERNAL "Have symbol memset") + set(LIBC_HAS_MODF "1" CACHE INTERNAL "Have symbol modf") + set(LIBC_HAS_MODFF "1" CACHE INTERNAL "Have symbol modff") + set(LIBC_HAS_POW "1" CACHE INTERNAL "Have symbol pow") + set(LIBC_HAS_POWF "1" CACHE INTERNAL "Have symbol powf") + set(LIBC_HAS_PUTENV "1" CACHE INTERNAL "Have symbol putenv") + set(LIBC_HAS_REALLOC "1" CACHE INTERNAL "Have symbol realloc") + set(LIBC_HAS_RINDEX "" CACHE INTERNAL "Have symbol rindex") + set(LIBC_HAS_ROUND "1" CACHE INTERNAL "Have symbol round") + set(LIBC_HAS_ROUNDF "1" CACHE INTERNAL "Have symbol roundf") + set(LIBC_HAS_SCALBN "1" CACHE INTERNAL "Have symbol scalbn") + set(LIBC_HAS_SCALBNF "1" CACHE INTERNAL "Have symbol scalbnf") + set(LIBC_HAS_SETENV "1" CACHE INTERNAL "Have symbol setenv") + set(LIBC_HAS_SIGNAL_H "1" CACHE INTERNAL "Have include signal.h") + set(LIBC_HAS_SIN "1" CACHE INTERNAL "Have symbol sin") + set(LIBC_HAS_SINF "1" CACHE INTERNAL "Have symbol sinf") + set(LIBC_HAS_SQR "" CACHE INTERNAL "Have symbol sqr") + set(LIBC_HAS_SQRT "1" CACHE INTERNAL "Have symbol sqrt") + set(LIBC_HAS_SQRTF "1" CACHE INTERNAL "Have symbol sqrtf") + set(LIBC_HAS_SSCANF "1" CACHE INTERNAL "Have symbol sscanf") + set(LIBC_HAS_STDARG_H "1" CACHE INTERNAL "Have include stdarg.h") + set(LIBC_HAS_STDBOOL_H "1" CACHE INTERNAL "Have include stdbool.h") + set(LIBC_HAS_STDDEF_H "1" CACHE INTERNAL "Have include stddef.h") + set(LIBC_HAS_STDINT_H "1" CACHE INTERNAL "Have include stdint.h") + set(LIBC_HAS_STDIO_H "1" CACHE INTERNAL "Have include stdio.h") + set(LIBC_HAS_STDLIB_H "1" CACHE INTERNAL "Have include stdlib.h") + set(LIBC_HAS_STRCASESTR "1" CACHE INTERNAL "Have symbol strcasestr") + set(LIBC_HAS_STRCHR "1" CACHE INTERNAL "Have symbol strchr") + set(LIBC_HAS_STRCMP "1" CACHE INTERNAL "Have symbol strcmp") + set(LIBC_HAS_STRINGS_H "1" CACHE INTERNAL "Have include strings.h") + set(LIBC_HAS_STRING_H "1" CACHE INTERNAL "Have include string.h") + set(LIBC_HAS_STRLCAT "1" CACHE INTERNAL "Have symbol strlcat") + set(LIBC_HAS_STRLCPY "1" CACHE INTERNAL "Have symbol strlcpy") + set(LIBC_HAS_STRLEN "1" CACHE INTERNAL "Have symbol strlen") + set(LIBC_HAS_STRNCMP "1" CACHE INTERNAL "Have symbol strncmp") + set(LIBC_HAS_STRNLEN "1" CACHE INTERNAL "Have symbol strnlen") + set(LIBC_HAS_STRNSTR "" CACHE INTERNAL "Have symbol strnstr") + set(LIBC_HAS_STRPBRK "1" CACHE INTERNAL "Have symbol strpbrk") + set(LIBC_HAS_STRRCHR "1" CACHE INTERNAL "Have symbol strrchr") + set(LIBC_HAS_STRSTR "1" CACHE INTERNAL "Have symbol strstr") + set(LIBC_HAS_STRTOD "1" CACHE INTERNAL "Have symbol strtod") + set(LIBC_HAS_STRTOK_R "1" CACHE INTERNAL "Have symbol strtok_r") + set(LIBC_HAS_STRTOL "1" CACHE INTERNAL "Have symbol strtol") + set(LIBC_HAS_STRTOLL "1" CACHE INTERNAL "Have symbol strtoll") + set(LIBC_HAS_STRTOUL "1" CACHE INTERNAL "Have symbol strtoul") + set(LIBC_HAS_STRTOULL "1" CACHE INTERNAL "Have symbol strtoull") + set(LIBC_HAS_SYS_TYPES_H "1" CACHE INTERNAL "Have include sys/types.h") + set(LIBC_HAS_TAN "1" CACHE INTERNAL "Have symbol tan") + set(LIBC_HAS_TANF "1" CACHE INTERNAL "Have symbol tanf") + set(LIBC_HAS_TIME_H "1" CACHE INTERNAL "Have include time.h") + set(LIBC_HAS_TRUNC "1" CACHE INTERNAL "Have symbol trunc") + set(LIBC_HAS_TRUNCF "1" CACHE INTERNAL "Have symbol truncf") + set(LIBC_HAS_UNSETENV "1" CACHE INTERNAL "Have symbol unsetenv") + set(LIBC_HAS_VFORK "1" CACHE INTERNAL "Have symbol vfork") + set(LIBC_HAS_VSNPRINTF "1" CACHE INTERNAL "Have symbol vsnprintf") + set(LIBC_HAS_VSSCANF "1" CACHE INTERNAL "Have symbol vsscanf") + set(LIBC_HAS_WCHAR_H "1" CACHE INTERNAL "Have include wchar.h") + set(LIBC_HAS_WCSCMP "1" CACHE INTERNAL "Have symbol wcscmp") + set(LIBC_HAS_WCSDUP "1" CACHE INTERNAL "Have symbol wcsdup") + set(LIBC_HAS_WCSLCAT "" CACHE INTERNAL "Have symbol wcslcat") + set(LIBC_HAS_WCSLCPY "" CACHE INTERNAL "Have symbol wcslcpy") + set(LIBC_HAS_WCSLEN "1" CACHE INTERNAL "Have symbol wcslen") + set(LIBC_HAS_WCSNCMP "1" CACHE INTERNAL "Have symbol wcsncmp") + set(LIBC_HAS_WCSNLEN "1" CACHE INTERNAL "Have symbol wcsnlen") + set(LIBC_HAS_WCSSTR "1" CACHE INTERNAL "Have symbol wcsstr") + set(LIBC_HAS_WCSTOL "1" CACHE INTERNAL "Have symbol wcstol") + set(LIBC_HAS_WCSTOLL "1" CACHE INTERNAL "Have symbol wcstoll") + set(LIBC_HAS_WCSTOUL "1" CACHE INTERNAL "Have symbol wcstoul") + set(LIBC_HAS_WCSTOULL "1" CACHE INTERNAL "Have symbol wcstoull") + set(LIBC_HAS__EXIT "1" CACHE INTERNAL "Have symbol _Exit") + set(LIBC_HAS__I64TOA "" CACHE INTERNAL "Have symbol _i64toa") + set(LIBC_HAS__LTOA "" CACHE INTERNAL "Have symbol _ltoa") + set(LIBC_HAS__STRREV "" CACHE INTERNAL "Have symbol _strrev") + set(LIBC_HAS__ULTOA "" CACHE INTERNAL "Have symbol _ultoa") + set(LIBC_HAS__WCSDUP "1" CACHE INTERNAL "Have symbol _wcsdup") + set(LIBC_IS_GLIBC "" CACHE INTERNAL "Have symbol __GLIBC__") + set(HAVE_GCC_WALL "1" CACHE INTERNAL "Test HAVE_GCC_WALL") + set(HAVE_GCC_WUNDEF "1" CACHE INTERNAL "Test HAVE_GCC_WUNDEF") + set(HAVE_GCC_WFLOAT_CONVERSION "1" CACHE INTERNAL "Test HAVE_GCC_WFLOAT_CONVERSION") + set(HAVE_GCC_NO_STRICT_ALIASING "1" CACHE INTERNAL "Test HAVE_GCC_NO_STRICT_ALIASING") + set(HAVE_GCC_WDOCUMENTATION "1" CACHE INTERNAL "Test HAVE_GCC_WDOCUMENTATION") + set(HAVE_GCC_WDOCUMENTATION_UNKNOWN_COMMAND "1" CACHE INTERNAL "Test HAVE_GCC_WDOCUMENTATION_UNKNOWN_COMMAND") + set(HAVE_GCC_COMMENT_BLOCK_COMMANDS "1" CACHE INTERNAL "Test HAVE_GCC_COMMENT_BLOCK_COMMANDS") + set(HAVE_GCC_WSHADOW "1" CACHE INTERNAL "Test HAVE_GCC_WSHADOW") + set(HAVE_GCC_WUNUSED_LOCAL_TYPEDEFS "1" CACHE INTERNAL "Test HAVE_GCC_WUNUSED_LOCAL_TYPEDEFS") + set(HAVE_GCC_WIMPLICIT_FALLTHROUGH "1" CACHE INTERNAL "Test HAVE_GCC_WIMPLICIT_FALLTHROUGH") + set(HAVE_GCC_FVISIBILITY "" CACHE INTERNAL "Test HAVE_GCC_FVISIBILITY") + set(HAVE_ST_MTIM "1" CACHE INTERNAL "Test HAVE_ST_MTIM") + set(HAVE_LD_VERSION_SCRIPT "1" CACHE INTERNAL "Test HAVE_LD_VERSION_SCRIPT") + set(HAVE_WL_VERSION_SCRIPT "1" CACHE INTERNAL "Test HAVE_WL_VERSION_SCRIPT") + set(LINKER_SUPPORTS_VERSION_SCRIPT "1" CACHE INTERNAL "Test LINKER_SUPPORTS_VERSION_SCRIPT") + set(LINKER_SUPPORTS_WL_NO_UNDEFINED "1" CACHE INTERNAL "Test LINKER_SUPPORTS_WL_NO_UNDEFINED") + set(ICONV_IN_LIBC "1" CACHE INTERNAL "Test ICONV_IN_LIBC") + set(ICONV_IN_LIBICONV "" CACHE INTERNAL "Test ICONV_IN_LIBICONV") + set(HAVE_GETPAGESIZE "1" CACHE INTERNAL "Have symbol getpagesize") + set(HAVE_SIGACTION "1" CACHE INTERNAL "Have symbol sigaction") + set(HAVE_SA_SIGACTION "1" CACHE INTERNAL "Have symbol sa_sigaction") + set(HAVE_SETJMP "1" CACHE INTERNAL "Have symbol setjmp") + set(HAVE_NANOSLEEP "1" CACHE INTERNAL "Have symbol nanosleep") + set(HAVE_GMTIME_R "1" CACHE INTERNAL "Have symbol gmtime_r") + set(HAVE_LOCALTIME_R "1" CACHE INTERNAL "Have symbol localtime_r") + set(HAVE_NL_LANGINFO "1" CACHE INTERNAL "Have symbol nl_langinfo") + set(HAVE_SYSCONF "1" CACHE INTERNAL "Have symbol sysconf") + set(HAVE_SYSCTLBYNAME "" CACHE INTERNAL "Have symbol sysctlbyname") + set(HAVE_GETAUXVAL "1" CACHE INTERNAL "Have symbol getauxval") + set(HAVE_ELF_AUX_INFO "" CACHE INTERNAL "Have symbol elf_aux_info") + set(HAVE_POLL "" CACHE INTERNAL "Have symbol poll") + set(HAVE_MEMFD_CREATE "1" CACHE INTERNAL "Have symbol memfd_create") + set(HAVE_POSIX_FALLOCATE "1" CACHE INTERNAL "Have symbol posix_fallocate") + set(HAVE_DLOPEN_IN_LIBC "1" CACHE INTERNAL "Have symbol dlopen") + set(HAVE_GETHOSTNAME "1" CACHE INTERNAL "Have symbol gethostname") + set(HAVE_SIGTIMEDWAIT "1" CACHE INTERNAL "Have symbol sigtimedwait") + set(HAVE_PPOLL "1" CACHE INTERNAL "Have symbol ppoll") + set(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR "" CACHE INTERNAL "Have symbol addchdir") + set(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP "" CACHE INTERNAL "Have symbol addchdir_np") + set(HAVE_FDATASYNC "1" CACHE INTERNAL "Have symbol fdatasync") + set(HAVE_GETRESUID "1" CACHE INTERNAL "Have symbol getresuid") + set(HAVE_GETRESGID "1" CACHE INTERNAL "Have symbol getresgid") + endfunction() +endif() diff --git a/cmake/macros.cmake b/cmake/macros.cmake index e0df1669c2..6862dd7790 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -425,7 +425,7 @@ function(SDL_PrintSummary) message(STATUS "") endif() - if(UNIX AND NOT (ANDROID OR APPLE OR EMSCRIPTEN OR HAIKU OR RISCOS OR DJGPP OR CYGWIN)) + if(UNIX AND NOT (ANDROID OR APPLE OR EMSCRIPTEN OR HAIKU OR RISCOS OR DJGPP OR CYGWIN OR OHOS)) if(NOT (HAVE_X11 OR HAVE_WAYLAND)) if(NOT SDL_UNIX_CONSOLE_BUILD) message(FATAL_ERROR diff --git a/cmake/sdlcompilers.cmake b/cmake/sdlcompilers.cmake index 87b00bbd70..932f161d6c 100644 --- a/cmake/sdlcompilers.cmake +++ b/cmake/sdlcompilers.cmake @@ -184,6 +184,11 @@ function(SDL_AddCommonCompilerFlags TARGET) if(USE_TCC) sdl_target_compile_option_all_languages(${TARGET} "-DSTBI_NO_SIMD") endif() + + if(OHOS) # OpenHarmony/HarmonyOS + # Stop the `clang: warning: argument unused during compilation: '--gcc-toolchain=$X/sdk/default/openharmony/native/llvm' [-Wunused-command-line-argument]` on every file compiled. I think this is a toolchain file issue...? + sdl_target_compile_option_all_languages(${TARGET} "-Wno-unused-command-line-argument") + endif() endfunction() function(check_x86_source_compiles BODY VAR) diff --git a/docs/README-harmonyos.md b/docs/README-harmonyos.md new file mode 100644 index 0000000000..d7b6f7a384 --- /dev/null +++ b/docs/README-harmonyos.md @@ -0,0 +1,872 @@ +# HarmonyOS + +(And also OpenHarmony.) + +## What is HarmonyOS? + +(This is my current understanding, please correct these docs if incorrect.) + +HarmonyOS is an operating system for tablets and phones. It is developed by +[Huawei](https://www.huawei.com/en/). It is competitive with platforms like +Android and iOS, capable of powering high-end phones. Unlike Google and Apple, +Huawei is a Chinese company, and HarmonyOS is largely serving the Chinese +market at this time. + +The system uses some custom pieces, but also relies on open source software +from outside of the project, such as NodeJS, musl, etc. + +Much of the platform is open source, under the name "OpenHarmony," and Huawei's +proprietary spin of OpenHarmony is called HarmonyOS. Huawei is the primary +contributor to OpenHarmony. + +So it is probably best said: HarmonyOS is to Android what OpenHarmony is to +AOSP. But, to be clear, OpenHarmony is not derived from Android. + +This document will generally refer to HarmonyOS, but from a development +viewpoint, you are actually targeting OpenHarmony, even if you're building +with intent to only target HarmonyOS on Huawei-produced phones. + + +## Supported versions + +All testing to date has been done on a real phone, a Huawei Mate 80. SDL +currently supports HarmonyOS 5.0.4 (API level 16) and later, although low-end +testing hasn't been done so there might be little changes needed for extreme +compatibility. Report bugs or send patches if this becomes a problem. + + +## Some basic target platform truths + +More or less, an app on HarmonyOS is a NodeJS process, in the same way that +an app on Android is a Java virtual machine process. While one generally +writes apps in Java or Kotlin for Android, HarmonyOS uses a TypeScript +variant called ArkTS. However, like SDL on Android, we will be focusing on +native code in C or C++, so SDL makes moves similar to its Android strategy, +migrating from ArkTS to native code as quickly as possible and handing control +over to a (presumably C or C++) native code app, and making it so the app can +entirely opt out of interacting with the ArkTS layer directly. + +The equivalent of the Android "JNI" layer is NodeJS's NAPI interfaces. A large +portion of the system APIs are accessible directly from OS-provided native +libraries, so there is a lot less reason to dip back down into the ArkTS layer +for most tasks, whereas lots of things on Android need to talk to Java-only +interfaces to accomplish common and important tasks, even in an +otherwise-entirely native program. + +The C/C++ compiler is based on Clang (you might end up using a compiler called +"BiSheng", which is Clang with some custom optimization techniques developed +by Huawei, but the binary is still called "clang"). + +The platform triplet is "aarch64-linux-ohos" (other CPU architectures are +possible, but this is likely the most common. The +devtools mention 32-bit ARM and x86-64, too). The CMake toolchain file sets +the variable `OHOS` (OpenHarmony OS) to "OHOS", so `if(OHOS)` works. Further, +`CMAKE_SYSTEM_NAME` is set to "OHOS". The C preprocessor defines `__OHOS__`. + + +## Getting hardware to run HarmonyOS + +(This information is accurate as of September 2026, but may be out of date at +any moment, so please check other sources before assuming this is still +correct.) + +HarmonyOS phones work in America (at least on WiFi; presumably one could get a +working (e)SIM card for phone calls and cell network access, but I haven't +tried). It may be difficult to obtain a phone: I couldn't find one for +purchase on Amazon or eBay from the United States. You might have an easier +time in other countries. I assume it is super-easy in China. :) + +One can run OpenHarmony (not HarmonyOS) on a Raspberry Pi 4b, compliments of +the Oniro project, which provides both source code and prebuilt images for +download: + +https://docs.oniroproject.org/device-development/developer-boards/raspberry-pi-4model-b/ + +This lets you run the open source variant of the OS on real hardware, but +obviously a Raspberry Pi isn't a tablet or a phone, so the experience is +somewhat tangential to the the most common configuration one might expect in +the field, but it _does_ work. + +DevEco Studio offers phone, tablet, and smartwatch emulators for Huawei +hardware, and these apparently run on Windows, macOS and Linux, but will not +let you download them outside of mainland China (except for smartwatch +emulators, for some reason). Comments on Huawei's forums suggest relaxing this +restriction is planned, but no timeline has been announced. + + +## Get HarmonyOS development tools + +You will need to download Huawei's developer tools. They offer a full IDE, +called "DevEco Studio," but this is not strictly necessary for compiling SDL +itself. + +There are also command line tools available for Windows, macOS, and Linux, and +these _are_ sufficient to compile SDL. For building full applications, you +likely need DevEco Studio to at least set up your project and manage signing +keys, etc. Some things (but not all) are doable from the command line tools, +and a handful of things are (currently) not. + +DevEco Studio, the GUI, is only available for Windows and macOS. It appears to +be a fork of IntelliJ IDEA. It does not appear to be open source, afaict. + +To download the developer tools, you'll need to register an account at +[the Huawei developer site](https://developer.huawei.com/en/). Then visit +[the tool download page](https://developer.huawei.com/consumer/en/download/) +For Linux, scroll down to "Command Line Tools." Download for your platform, +which should be a simple .zip file, and unpack it. If you're on Windows or +macOS, download and install DevEco Studio itself while you're here, too. + + +## Don't fight against DevEco Studio + +I'm a Linux user, and I avoid programming IDEs in general, so my instinct is +to just get everything done with my text editor and the command line tools, +and this _feels_ possible, but I haven't quite figured it out. The system is +friendly to things like CMake, clang, etc, and the documentation, even in +English, is pretty thorough, but I still couldn't get there without using +DevEco Studio a little. + +The system appears designed to at least hand off a working project from +DevEco Studio to a Linux machine for building and deploying from the command +line tools, for Continuous Integration if nothing else. Figuring out how to +eliminate the Windows/Mac GUI dependency entirely is an ongoing and unresolved +concern. I definitely needed it to manage signing keys. Once I got that far, I +was able to copy the appropriate directories to a Linux system and use the +`hvigorw` command line tool to build and sign an app, and `hdc` to install it +on a real phone. Please see "The Helper Script" section, below, to automate +a lot of the Linux command line pieces, too. + + +## Install command line tools. + +(If you're entirely focused on DevEco Studio, you can probably skip this.) + +I unzipped the downloaded command line tools so they're sitting in +`$HOME/HarmonyOS-SDK/command-line-tools`, but anywhere you want to put it is +fine. + +When this document refers to this directory, it'll call it `$CLT` for short. + +Add some things to your PATH, so you can find `hdc` and `hvigorw`, etc. + +```bash +export PATH=$PATH:$CLT/sdk/default/openharmony/toolchains:$CLT/bin +``` + + +## Putting a HarmonyOS phone in developer mode. + +If you want to test and debug your SDL apps natively on a real phone directly, +you have to put the device in Developer Mode. As the name implies, this is not +necessary to install published apps, but only apps under development locally. +Any retail phone running HarmonyOS should be able to do this. This is almost +identical to how one would do this for local Android development. + +On the phone, go to Settings, Device Name (it's "Mate 80" on mine, in a box by +itself near the top of the main settings page). This will bring up an "About" +page. Scroll down to "Software version" and tap the version number seven times +quickly. The phone will pop up a dialog asking if you want to enable developer +mode and reboot. Say yes. If you've already done this, it will tell you so. + +After reboot, go into Settings, System. There will be a new "Developer options" +section at the bottom. Turn on USB debugging. Plug in a USB cable to your +computer, and choose to trust the connection (and choose "use USB to transfer +files" if given the option, otherwise it will only allow charging over USB). + +At this point, you should be able to use "hdc" somewhat like you would use +"adc" on an Android device: + +See if the phone is visible/accessible: + +```bash +hdc list targets -v +``` + +You can also connect the phone over Wifi on the same LAN and not use USB. If +you want to try, turn on "Wireless debugging" in Developer Options, note the +IP address and port, and run: + +```bash +hdc tconn 192.168.1.104:40317 +``` + +Where the IP address and port match the phone. + +(DevEco Studio: add the device by IP address and port by going to "Tools" -> +"IP Connection" on the menu bar. This appears to be IPv4 only at the moment, +but it successfully connected to the phone. This was a good option, because +for whatever reason the phone's USB connection would continually drop and +reconnect on my VirtualBox Windows 10 VM. But YMMV!) + + +## HarmonyOS App Development with SDL + +### Create a new project + +Copy the "openharmony-project" directory out of the root of SDL's source tree +to where you want your project to live, and name it appropriately +("mygame-openharmony" or whatever). Then, in that new directory: + +- in AppScope/app.json5, change at least "bundleName" and "vendor" to match + your project. +- in AppScope/resources/base/element/string.json, change "app_name"'s value to + the name of your app. +- in build-profile.json5, you need to add a signingConfig (this is probably + doable without the GUI, but I have not entirely solved how to do this outside + of DevEco Studio yet). + +Do not create a new project in DevEco Studio, since there are pieces of ArkTS +scripting that need to be set up correctly for SDL, and that code is already +present in openharmony-project. You _should_ be able to open the project in +DevEco Studio once you've set it up. + + +### Add source code to your new project. + +Native code building uses CMake, whether you use DevEco Studio or not. This +makes integrating SDL into your project easy! + +Edit the text file entry/src/main/cpp/CMakeLists.txt inside your new project +directory and change this line: + +```cmake +add_library(main SHARED myapp.c) +``` + +"myapp.c" should be replaced with any source files that comprise your app. +This is a standard CMake project file, so you can get as fancy as you want +or just have a list of files separated by spaces. + +These files do not have to be in this directory; they can live outside of +the project directory entirely, as long as you specify the correct directory +to reach them. + +You must either copy the SDL source tree into entry/src/main/cpp, or make a +symlink to it, or edit the `add_subdirectory("SDL")` line to point to it. +This line makes SDL's source compile with the rest of the project. The +directory you point to must be the root of an SDL clone, the one that holds +CMakeLists.txt, src, and include. + +Your app _must_ compile to a shared library named "libmain.so" ... the +"add_library" line does this. Do not build an executable! It must be a +shared library, due to how HarmonyOS deals with apps. When you're done +building, you should have a libmain.so and a libSDL3.so file. + + +### Customize your project + +If you need some specific permissions (microphone/camera access, internet +access, etc), you must list them in +openharmony-project/entry/src/main/module.json5 ...some of these are +automatically granted on installation, some will require the user to +approve them on first use at runtime, but all of them must be listed. See +the "requestPermissions" section of this file for examples. There are more +permissions available than are listed there! + +You must set up some basic strings, before publishing your app, or you will +either have an app icon with name "label" or fail certification because you +didn't explain clearly why you need various permissions. + +These are in a JSON file in: + +openharmony-project/entry/src/main/resources/base/element/string.json + +You can do localizations of these strings by making a second copy of the +file, where "base" is replaced with the proper localization ("en_GB" or +whatever). See the documentation at: + +https://developer.huawei.com/consumer/en/doc/harmonyos-guides/resource-categories-and-access + +Static data files that ship with your app go in the directory: + +openharmony-project/entry/src/main/resources/rawfile/ + +The file tree under this directory will be packaged up with the app and made +available through SDL APIs (see "Filesystem" and "IOStream" sections, below). + + +### Get a HarmonyOS Debug Certificate + +You need to sign apps to run them on a real device, even for local debugging +and testing and not just deployment and release. + +For now, just open your project in DevEco Studio, then in the menu bar, go +to "File", "Project Structure", "Signing Configs", "Automatically generate +signature". Log in to your Huawei developer account if needed. Let it do the +work. Later, I copied the project's directory and ".ohos" dir in my home +directory to a Linux system, adjusted a few Windows absolute paths in +build-profile.json5, and that was good enough. + + +### Build your app from the command line + +Building your project from DevEco Studio is just a single click, but you +can also build from the command line on macOS, Windows, or Linux. + +If you have everything (signing certs, the entire project structure, etc) in +place, you can build an app from the command line. The HarmonyOS equivalent of +Android's `gradlew` command is `hvigorw`. Go to the root folder of the project +and run: + +```bash +hvigorw assembleHap +``` + +If everything went well (AND IT OFTEN DOES NOT, READ THE OUTPUT!), you'll have +a signed HarmonyOS app bundle (a ".hap" file) you can install to a device. + +There is a Bash shell script to automate this as much as possible: see the +"The Helper Script" section, below. + + +### Install on real hardware from the command line. + +Make sure the phone is connected (run `hdc list targets -v` to verify). + +```bash +# (or whatever the directory and .hap file are named.) +hdc install ./entry/build/default/outputs/default/entry-default-signed.hap +``` + +There is a Bash shell script to automate this as much as possible: see the +"The Helper Script" section, below. + + +### Debugging + +If you just want to do "printf debugging," then SDL_Log() will write to the +system's "hilog" output, and you can use `hdc hilog |grep -F "APPBUNDLENAME"` +to find your app's output in the absolute firehose of text that is produced +through that interface. + +One can use a real GUI debugger through DevEco Studio without a lot of drama, +and one can also use lldb from the command line with "remote debugging." The +latter option takes a lot of manual setup, and additionally you have to be +familiar with LLDB's text interface to find this useful, but it _can_ be done +from Linux, where DevEco Studio isn't available (and also Windows and macOS, +if all you have are the command line tools). + +There is a Bash shell script to automate this as much as possible: see the +"The Helper Script" section, below. + +The instructions on setup from scratch, without the shell script, are here: + +https://developer.huawei.com/consumer/en/doc/harmonyos-guides/debug-lldb#remote-debugging + +The only thing that document fails to mention is that the PID number you need +for LLDB's `attach` command can be obtained by running `hdc shell ps` and +looking for your app's bundle ID. + + +### The Helper Script + +There is a Bash shell script to simplify several tasks, if you're using the +command line tools. This is not necessary from DevEco Studio. This has only +been tested on Linux. + +Make sure your PATH is set up (see "Install Command Line Tools," above), and +run this command: + +```bash +SDL/build-scripts/harmonyos-tool.sh openharmony-project --build --debug +``` + +"openharmony-project" is literally the path to your project file. It can be a +relative path, including "." if you are sitting in the directory. + +All the other command line arguments are optional, and can be used in any +combination (for example, you can --launch without --build'ing, or --install +and --debug in one run, etc.) + +If you specify --log, the script will fire up `hdc hilog` in the background +and filter out everything but your app bundle's log calls, so you can watch +the log while other tasks complete. It will start this once passed the +--install stage (even if you didn't install), so you are watching it while +launching/debugging begins. After all other tasks are done, harmonyos-tool.sh +won't terminate until either the `hdc` process terminates or the user hits +CTRL-C (rather, until SIGINT is delivered, which CTRL-C does). The script will +kill the `hdc` process in that case before terminating itself. + +If you specify --clean, the script will clean up any build artifacts from +previous compiles, etc. This will run before --build, so you can specify +`--clean --build` for a complete rebuild. + +If you specify --build, the script will compile C/C++ code and assemble the +.hap file to be run on a device or emulator. + +If you specify --install, the script will (re)install the .hap on a device +(which will kill the process if it's currently running). + +If you specify --uninstall, the script will remove the existing app on a +device (which will kill the process if it's currently running). + +If you specify --launch, the script will launch the app on a device, if it +isn't already running. + +If you specify --debug, the script will prepare remote debugging via lldb, +and attach to the running app. + +If you specify --kill, the script will force-stop any existing process. + +(--debug implies --launch, "--build --launch" and "--build --debug" imply +--install.) + +Most of these tasks obviously need a phone that hdc can talk to. + +Note that debugging always attaches to a process, so if you need to debug +something at startup, plan to add a sleep to the start of the program, as it +can take several seconds for the attachment to complete. It isn't clear to me +how to launch an app from the debugger so you can break at startup, but there +is probably a way. + +Also note that this script is fragile in general; be gentle, report bugs, send +patches. + + +## SDL/HarmonyOS subsystem details + +Specifics of actually using SDL on HarmonyOS follow. + + +### SDL platform defines + +SDL will define SDL_PLATFORM_OPENHARMONY and SDL_PLATFORM_UNIX. Do be cautioned +that while HarmonyOS has some Linux kernel compatibility interfaces, it is +_not_ comparable to a "normal" Linux system in almost any way, so +SDL_PLATFORM_LINUX is not defined (SDL for Android, which literally uses the +Linux kernel, follows this same convention inside SDL). HarmonyOS devices +don't use the actual Linux kernel (they use a microkernel named "HongMeng"), +but OpenHarmony devices that aren't specifically HarmonyOS _might_ use a Linux +kernel. Don't make assumptions. + + +### Dynamic API + +Presumably users cannot override the SDL build in their phone's apps, so the +Dynamic API is disabled on this platform. + + +### Log + +SDL_Log() and friends will push through hilog, so you can view the output +from the `hdc hilog` command. Note that logs are preformatted strings by the +time they hit hilog, so things like "%s" are already processed, and thus +there is no `{public}` and `{private}` format modifiers available like there +would be if calling into hilog's APIs directly. + +Output from `hdc hilog` for `SDL_Log("Hello world!");` will look something like: + +``` +06-29 22:22:02.953 25275 25275 I A00000/org.libsdl.loopwave/SDL/APP: Hello world! +``` + +This is kind of chatty, but not unlike Android's `adc logcat` output. + +These logs are viewable to end-users, even in release builds, if they have a +phone in Developer Mode that can talk to `hdc`, so be careful what you log! + +Also plan to use "grep" to find relevant logs; `hdc hilog` is an endless +waterfall of unrelated data that you _will_ get lost in immediately. + + +### Main + +HarmonyOS has a fairly complicated startup sequence (described later in this +document). Apps for this platform should use the "main callbacks" (with +`#define SDL_USE_MAIN_CALLBACKS`) instead of an ANSI C style "main" entry +point, as that fits the app design paradigm of OpenHarmony. + +However, if you build an app with a standard "main" function, SDL will notice +this, spin a thread, and attempt to call that function from the new background +thread, and call exit() when it returns. This _happens_ to work, at least for +simple test cases that do rendering and touch input, but one uses this path at +their own risk, as several important things might result in unexpected race +conditions and unexpected behavior, possibly in a later version of the OS. It +is strongly recommended that you migrate to the Main Callbacks! + + +### Filesystem + +HarmonyOS uses '/' as a path separator, like Unix-style (etc) systems. + +SDL_GetBasePath() returns "assets://" unconditionally (see IOStream section, +below). SDL_GetPrefPath() returns a path under the string returned from +OH_AbilityRuntime_ApplicationContextGetFilesDir(), which in practice looks +like "/data/storage/el2/base/files/APPNAME" ... this looks like an absolute +path, but HarmonyOS maps this to an app-and-user-specific path behind the +scenes. The prefpath is readable/writable with both SDL_IOStream and "normal" +APIs like fopen(). + +There is also SDL_GetOpenHarmonyInternalStoragePath(), which returns the base +directory used for SDL_GetPrefPath() without extra subdirs appended to it or +mkdir() calls issued, but generally SDL_GetPrefPath() is preferred as the +more-portable option. + + +### IOStream + +This works like Android: some files are accessible like normal filesystem +things, and can be accessed with fopen(), open(), etc. Other things are +installed with the application (the "HAP"), and they need special APIs to +access, that sort of map to Android's AAssetManager, but a little more +powerful (these are called "RawFile" APIs on HarmonyOS). + +Both types of files can be accessed as SDL_IOStreams through SDL_IOFromFile(). +SDL uses the same conventions as Android here: if the file path is a relative +path or prefixed with "assets://", it'll use the RawFile APIs. Absolute paths +will use "normal" filesystem APIs. SDL_GetBasePath() returns "assets://", so +one can access files installed with the app with SDL_IOFromFile(). + +"assets://" is entirely an SDL3 construct; it will not work with fopen(), and +is not understood by the system itself. + + +### Async I/O + +SDL uses the "generic" asyncio backend here (a thread pool that uses +SDL_IOStreams). Not only is there no sort of io_uring-style interface (afaik), +but this also means you can async-load stuff from "assets://" paths without +problems. + + +### Video + +One SDL_Window is allowed at a time and it takes the entire available display +(like Android). Currently we only report a single display (the phone/tablet's +screen). + +Setting an SDL_Window to be fullscreen means it will turn off the system +status and navigation bars, allowing the window to use those extra pixels. A +non-fullscreen window leaves those bars in place, letting users see the +current time, battery/network/etc status, and other icon things. + +Touch events work, and multitouch is supported. Mouse events (from an external, +physical mouse) are supported. Physical keyboard input works, and enabling +text input will pop up a screen keyboard. + +Clipboard is supported for text. SDL_SetClipboardText() works as expected +and other apps will see the text. However, an app needs the +`ohos.permission.READ_PASTEBOARD` permission to obtain data from the system +clipboard, and this is a _restricted_ permission: you must list it in +modules.json5, prompt the user to approve it beforehand by calling +SDL_RequestOpenHarmonyPermission(), and also the app developer must +explicitly apply for permission from Huawei through AppGallery Connect! +Consider if you actually need to read clipboard data into your app that badly. + +SDL_GetSystemTheme() and SDL_EVENT_SYSTEM_THEME_CHANGED both work. + + +### Render/GPU + +OpenGL ES 3 is supported, as is Vulkan. Desktop OpenGL is supported by +the platform, too, but for now support is disabled within SDL itself as likely +superfluous (until it is not, open an issue if you need it). EGL is used to +manage GL contexts internally, and a HarmonyOS-specific Vulkan extension is +provided to get a surface. + +Like other platforms, we favor OpenGL ES 2 for the 2D renderer; Vulkan and "GPU" +also works, but SDL's GLES is assumed to be more trusted at the current moment, +and historically GLES has been more solid on mobile devices than Vulkan, but +HarmonyOS-based phones might have significantly better Vulkan support than +other comparable smartphones; this is yet to be determined. + +Since Vulkan works, the SDL3 GPU API is available and operational, in addition +to the hardware-accelerated 2D Renderer API. + + +### Audio + +HarmonyOS support OpenSL ES, but like on Android, it is deprecated. As our +OpenSL ES backend is heavy with Android-specific code, it is not used on +HarmonyOS. There is a new backend using HarmonyOS's OHAudio API. This is +almost exactly like Android's AAudio API. + +Recording works, but you must request `ohos.permission.MICROPHONE` permission +before it will work. You must list it in modules.json5, and prompt the user to +approve it beforehand by calling SDL_RequestOpenHarmonyPermission(). Once +permission is granted, recording audio devices can be opened as usual, and +this permission remains for future runs, unless the user revokes in in the +system preferences later. On HarmonyOS, if a user denies permission, they will +never get the popup request dialog again, and must go and grant permission +through the system preferences. Plan accordingly if you want your UI to +explain this to users. + +Note that SDL for Android currently requests permission from the user on +behalf of the app when opening a recording device, and blocks until the user +approves or denies the request, but on HarmonyOS this is not done. This may +change in the future, but for now the app should handle requesting permission +before opening the device. If a future revision of SDL tries to manage the +permission, it won't hurt anything if the app has already taken care of it. + + +### Power + +Battery percent, and whether the device is plugged in, are reported. + + +### Thread + +OpenHarmony uses pthreads, and are fully supported. + + +### LoadSO + +HarmonyOS uses ELF shared libraries and has a POSIX-style dlopen() mechanism, +so SDL_LoadObject() works as expected. + + +### Tray + +The Tray API uses the "dummy" backend as this functionality doesn't make sense +on a phone. + + +### Time + +The Time and Timer subsystems use the usual Unix implementations and work fine. + + +### Locale + +SDL_GetPreferredLocales() returns a single locale, as specified in System +Preferences -> "System" -> "Language & Region". In my case, I have it set to +English/United States, so SDL reports "en_US". + +SDL will send SDL_EVENT_LOCALE_CHANGED events if the user changes their locale +in the system preferences while the app is running. + + +### Camera + +Camera access works. You must request the `ohos.permission.CAMERA` permission +in modules.json5. SDL will handle the UI to prompt the user for permission +during SDL_OpenCamera() if necessary, so the app doesn't have to manage this +further (unlike using a microphone in the audio API, SDL's camera API was +built on the idea that the user's permission or refusal might be coming at a +later time). + + +### Misc + +SDL_OpenURL() works, and will launch the appropriate app ("ability") for +whatever protocol is specified: "https://", etc, URLs will bring up a web +browser, custom URL schemes can launch specific apps, etc. + + +### STILL TODO + +- Message boxes +- Assert +- Dialog +- Haptic +- HIDAPI +- Joystick +- Process +- Sensor +- Storage +- Pen (tablets work over USB-C on the phone, but this isn't wired up in SDL at all) +- SDL_EVENT_DISPLAY_ORIENTATION (window resizes when phone orientation changes, this event doesn't fire yet, though) +- Can external displays be used over USB-C => HDMI? (yes, at least for mirroring) +- SDL_HINT_ORIENTATION (for runtime control; but you _can_ explicitly set the "orientation" setting in module.json5 for now if you need a specific global, unchanging setting.) + +## Building SDL for HarmonyOS + +(Skip this if you are developing an SDL-based app and not SDL itself. There's +a different section of this document for you, later.) + +SDL for HarmonyOS is built with CMake, like other platforms. Please note that +you _must_ use the CMake that comes with HarmonyOS's development tools, or +building will fail, as standard CMake does not recognize this platform at all +currently. + +Generate a project file for building SDL. We use Ninja, because it's a fast +command line tool that will maximize multi-CPU compiling, but it's probably +possible to generate a real Visual Studio, Xcode, etc, project. + +```bash +cd SDL +mkdir buildbot-ohos # or whatever you want to call the build directory. +cd buildbot-ohos +$CLT/sdk/default/openharmony/native/build-tools/cmake/bin/cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=$CLT/sdk/default/openharmony/native/build/cmake/ohos.toolchain.cmake -DCMAKE_BUILD_TYPE=Release -DOHOS_COMPATIBLE_SDK_VERSION=16.0.0 .. +``` + +(One can also add -DOHOS_ARCH=cputype, where "cputype" is "arm64-v8a" for ARM64, "armeabi-v7a" for ARM32, and "x86_64" Intel 64-bit. It defaults to ARM64 if unspecified.) + + +If all went well, you should be able to build SDL for HarmonyOS now. In our +case, build with Ninja: + +```bash +ninja +``` + +Or use whatever instructs Xcode/Visual Studio/etc to build things. + + + +## Startup sequence + +(You can probably skip this section if you just want to target HarmonyOS with +an SDL3-based app and not delve into the low-level details.) + +Starting an SDL app on HarmonyOS works differently than any other platform, +but efforts have been made to _hide_ most of this from the C programmer, so +this should mostly feel like any other SDL app they have built in C before. + +This is how startup works: + +(Exact paths might vary, and maybe this will simplify later.) + +- The actual application entry point is in ArkTS, in the source file + `$PROJECT/entry/src/main/ets/entryability/EntryAbility.ets`. +- This file defines a class, EntryAbility, that extends UIAbility, and + also imports the native SDL library. +- By now, libSDL3.so has been loaded. In a shared library constructor named + SDL_RegisterNativeInterfaces, it uses NAPI to register a native module. Once + this module initializes, we'll be able to call into SDL's C code from ArkTS + through interfaces we defined in SDL_Init_Native_Interfaces(). This is in + SDL/src/core/openharmony/SDL_openharmony.c. This will _also_ hook into + XComponent's API, to register some event callbacks. +- EntryAbility has an overridden method named onCreate. When this + runs, the app ("Ability" in HarmonyOS terms) is starting up. This function + calls into the sdl module we registered to hand it a few objects (including + the EntryAbility instance, and some small things that are trivial to create + in ArkTS. +- SDL, now in native code, will save off those objects as appropriate, and + also monkey-patch EntryAbility, so that it now has several overridden + methods that only exist in native code. This lets us deal with ArkTS-only + interfaces on EntryAbility that the system intends to interact with, without + providing any ArkTS scripting for them. +- EntryAbility now has an overridden method named onWindowStageCreate. When this + runs, the system is ready for the app to display to the screen. This + method, in C, calls `windowStage.loadContent('pages/Index', ...)`. +- This causes the app to load `$PROJECT/entry/src/main/ets/pages/Index.ets`. +- Index.ets specifies a layout for the window/view/whatever. We simply have a + single row and column containing a single "XComponent" which is more or + less a UI widget backed by native code. +- When the XComponent's OnSurfaceCreated callback fires (landing in + SDL_XComponent_OnSurfaceCreatedCallback()), we are then ready to hand control + to the actual C application. This starts in + SDL_OpenHarmonyMainSurfaceCreated(), which eventually lands in + RunAppOpenHarmonyMain(). +- Here we load libmain.so, which is where the app's actual C code lives. + If we see if libmain contains a symbol named "SDL_main", we spin a thread + and call into that symbol from the new thread as a standard ANSI C "main" + entry point, and exit() the process when it returns (which looks like a + crash on HarmonyOS, so please don't return.) +- If there was no symbol called "SDL_main", we look for the Main Callbacks + symbols in libmain.so: SDL_AppInit, SDL_AppIterate, etc. If found, we will + call libmain.so's SDL_AppInit() and respond appropriately. If not found, we + panic and terminate the process, since we're out of options and something + was obviously built incorrectly. +- Later, whenever the XComponent fires its OnFrame callback, we will call + SDL_OpenHarmonyOnFrameCallback(), which calls libmain.so's SDL_AppIterate(), + if we ended up using the Main Callbacks. + + + +## Incomplete notes + +Everything below here is incomplete, in-progress, and maybe incomprehensible. +You can stop reading now, if you like. + +- A lot of how to get started with native code and rendering was gleaned from + https://gitee.com/harmonyos_samples/ndk-opengl and then trying to find + various symbols used in there in the HarmonyOS documentation. + +- https://github.com/openharmony/app_samples/ETSUI/XComponent, seems to do + a lot of the native startup without the windowStage nonsense. Not clear if + this still works, or is safe to do, but it _would_ simplify things a little. + + +### Android mappings + +These are not direct mappings (the command lines are not identical, etc). + +- "adc" command line tool: "hdc" +- "adc logcat": "hdc hilog" +- "gradlew": "hvigorw" + + +### Get a HarmonyOS Debug Certificate without DevEco Studio + +This is a giant dump of notes for doing this manually, which I didn't _quite_ +manage to get working. Huawei has docs on this, which I've sort of half limped +through. + +For now, just create your project in DevEco Studio, +then in the menu bar, got to "File" -> "Project Structure" -> +"Signing Configs" -> "Automatically generate signature". Log in to your Huawei +developer account if needed. Let it do the work. Later, I copied the project's +directory and ".ohos" dir in my home directory to a Linux system, adjusted a +few Windows absolute paths in build-profile.json5, and that was good enough. + + +(Ignore these notes if you aren't improving them.) + +If you aren't using DevEco Studio to automatically generate signatures: + +You need a signing certificate before you can install apps on a real HarmonyOS +device. + +You get a certificate by generating a Certificate Signing Request (a "CSR") +and submitting it to Huawei. + +Right now this needs DevEco Studio to generate the CSR file, as a one-time +step. Go into the IDE, click the hamburger menu icon on the top left of the +window, choose the "Build" menu item, and then click "Generate Key and CSR". +In the dialog that pops up, you'll first make a signing key in ".p12" format. +Enter a filename and save path, choose a password for the key. Fill in the +"Alias" which is any short, unique, possibly-ASCII, human readable string. +Ignore the Advanced section. + +Click "Next". Now you'll generate the CSR file. The same .p12 file, password +and alias you just generated will be prefilled in the new dialog. Just tell it +where to save the new .csr file. Click "Finish" and you're done with DevEco +Studio for this task. + +The Common Name ("CN=" field) on the Certificate Request is what DevEco Studio +refers as an "alias." I put this in as "icculus.org" for mine, but it might +just need to be unique. It's required, all the other fields are not (indeed, +DevEco Studio does not ask for them by default and leaves them blank). + +*TODO IN THE FUTURE:* the CSR files are in a standard format, so OpenSSL's +command line utility can _probably_ generate them without having to go through +DevEco Studio, but anything OpenSSL is complicated and needs research. + +Dumping the .csr file that DevEco Studio generated (using the command line: +`openssl req -in MyCSRFile.csr -noout -text`) reports the following, so there +are probably OpenSSL incantations to generate your own .p12 and .csr file +directly. + +``` +Certificate Request: + Data: + Version: 1 (0x0) + Subject: C=, ST=, L=, O=, OU=, CN=[the "Alias" that I entered is here] + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + [hex values for public key are here] + ASN1 OID: prime256v1 + NIST CURVE: P-256 + Attributes: + Requested Extensions: + X509v3 Subject Key Identifier: + [hex values for key identifier are here] + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + [hex values for signature are here] +``` + +Once you have a .csr file, go to Huawei's AppGallery Connect portal, and find +the "Certificates, app IDs, and profiles" tab at the top. Click the "New +certificate" button, and upload the .csr file you generated. Give it a name, +set the "Certificate type" to "Debug certificate" and point to your .csr file. +Click "Submit". + +If everything went okay, you'll have a certificate listed for download. This +should be an immediate thing, it doesn't need approval on Huawei's side. +Download the .cer file and save it for later. + diff --git a/docs/README-platforms.md b/docs/README-platforms.md index 4c305190af..4f9a7d6502 100644 --- a/docs/README-platforms.md +++ b/docs/README-platforms.md @@ -9,6 +9,7 @@ SDL3 has been known to work on the following platforms at some point: - [Emscripten](README-emscripten.md) (Web browsers) - [FreeBSD](README-bsd.md) - [Haiku OS](README-haiku.md) +- [HarmonyOS/OpenHarmony](README-harmonyos.md) - [iOS](README-ios.md) - [Linux](README-linux.md) - [macOS](README-macos.md) (10.14 and later) diff --git a/include/SDL3/SDL_iostream.h b/include/SDL3/SDL_iostream.h index f369fde17f..3edde85a49 100644 --- a/include/SDL3/SDL_iostream.h +++ b/include/SDL3/SDL_iostream.h @@ -257,6 +257,10 @@ typedef struct SDL_IOStream SDL_IOStream; * to an Android NDK `AAsset *`, that this SDL_IOStream is using to access * the filesystem. If SDL used some other method to access the filesystem, * this property will not be set. + * - `SDL_PROP_IOSTREAM_OPENHARMONY_RAWFILE64_POINTER`: a pointer, that can be + * cast to an OpenHarmony/HarmonyOS `RawFile64 *`, that this SDL_IOStream is + * using to access the filesystem. If SDL used some other method to access + * the filesystem, this property will not be set. * * \param file a UTF-8 string representing the filename to open. * \param mode an ASCII string representing the mode to be used for opening @@ -277,10 +281,11 @@ typedef struct SDL_IOStream SDL_IOStream; */ extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromFile(const char *file, const char *mode); -#define SDL_PROP_IOSTREAM_WINDOWS_HANDLE_POINTER "SDL.iostream.windows.handle" -#define SDL_PROP_IOSTREAM_STDIO_FILE_POINTER "SDL.iostream.stdio.file" -#define SDL_PROP_IOSTREAM_FILE_DESCRIPTOR_NUMBER "SDL.iostream.file_descriptor" -#define SDL_PROP_IOSTREAM_ANDROID_AASSET_POINTER "SDL.iostream.android.aasset" +#define SDL_PROP_IOSTREAM_WINDOWS_HANDLE_POINTER "SDL.iostream.windows.handle" +#define SDL_PROP_IOSTREAM_STDIO_FILE_POINTER "SDL.iostream.stdio.file" +#define SDL_PROP_IOSTREAM_FILE_DESCRIPTOR_NUMBER "SDL.iostream.file_descriptor" +#define SDL_PROP_IOSTREAM_ANDROID_AASSET_POINTER "SDL.iostream.android.aasset" +#define SDL_PROP_IOSTREAM_OPENHARMONY_RAWFILE64_POINTER "SDL.iostream.openharmony.rawfile64" /** * Use this function to prepare a read-write memory buffer for use with diff --git a/include/SDL3/SDL_keyboard.h b/include/SDL3/SDL_keyboard.h index da0a4185b0..ba31c92cad 100644 --- a/include/SDL3/SDL_keyboard.h +++ b/include/SDL3/SDL_keyboard.h @@ -464,6 +464,12 @@ typedef enum SDL_Capitalization * use, overriding other properties. This is documented at * https://developer.android.com/reference/android/text/InputType * + * On HarmonyOS/OpenHarmony you can directly specify the input type: + * + * - `SDL_PROP_TEXTINPUT_OPENHARMONY_INPUTTYPE_NUMBER` - the text input type to + * use, overriding other properties. This is documented at + * https://developer.android.com/reference/android/text/InputType + * * \param window the window to enable text input. * \param props the properties to use. * \returns true on success or false on failure; call SDL_GetError() for more @@ -489,6 +495,7 @@ extern SDL_DECLSPEC bool SDLCALL SDL_StartTextInputWithProperties(SDL_Window *wi #define SDL_PROP_TEXTINPUT_DEFAULT_TEXT_STRING "SDL.textinput.default_text" #define SDL_PROP_TEXTINPUT_MAX_LENGTH_NUMBER "SDL.textinput.max_length" #define SDL_PROP_TEXTINPUT_ANDROID_INPUTTYPE_NUMBER "SDL.textinput.android.inputtype" +#define SDL_PROP_TEXTINPUT_OPENHARMONY_INPUTTYPE_NUMBER "SDL.textinput.openharmony.inputtype" /** * Check whether or not Unicode text input events are enabled for a window. diff --git a/include/SDL3/SDL_main.h b/include/SDL3/SDL_main.h index 64daa42c39..baccece0e8 100644 --- a/include/SDL3/SDL_main.h +++ b/include/SDL3/SDL_main.h @@ -193,6 +193,19 @@ */ #define SDL_MAIN_EXPORTED + #elif defined(SDL_PLATFORM_OPENHARMONY) + /* On HarmonyOS/OpenHarmony SDL does a lot of work to gain control of the app. */ + #define SDL_MAIN_NEEDED + + /* As this is launched from ArkTS, the real entry point (main() function) + is outside of the the binary built from this code. + This define makes sure that, unlike on other platforms, SDL_main.h + and SDL_main_impl.h export an `SDL_main()` function (to be called + from ArkTS, sort of), but don't implement a native + `int main(int argc, char* argv[])` or similar. + */ + #define SDL_MAIN_EXPORTED + #elif defined(SDL_PLATFORM_EMSCRIPTEN) /* On Emscripten, SDL provides a main function that converts URL parameters that start with "SDL_" to environment variables, so diff --git a/include/SDL3/SDL_main_impl.h b/include/SDL3/SDL_main_impl.h index bf5f583639..f5307ceb34 100644 --- a/include/SDL3/SDL_main_impl.h +++ b/include/SDL3/SDL_main_impl.h @@ -49,6 +49,13 @@ /* currently there are no platforms that _need_ a magic entry point here for callbacks, but if one shows up, implement it here. */ + #elif defined(SDL_PLATFORM_OPENHARMONY) + /* HarmonyOS/OpenHarmony: we look up SDL_AppInit/SDL_AppIterate/etc in libmain.so and call them directly. + Since we don't need a `main` entry point, we don't generate an SDL_main implementation here _at all_. */ + #ifndef SDL_MAIN_USE_CALLBACKS /* !!! FIXME, remove this later? */ + #error SDL3 on OpenHarmony currently requires the SDL Main Callbacks to operate. + #endif + #else /* use a standard SDL_main, which the app SHOULD NOT ALSO SUPPLY. */ /* this define makes the normal SDL_main entry point stuff work...we just provide SDL_main() instead of the app. */ diff --git a/include/SDL3/SDL_platform_defines.h b/include/SDL3/SDL_platform_defines.h index 4d13740073..db3d28af8c 100644 --- a/include/SDL3/SDL_platform_defines.h +++ b/include/SDL3/SDL_platform_defines.h @@ -112,6 +112,17 @@ #undef SDL_PLATFORM_LINUX #endif +#if defined(__OHOS__) + +/** + * A preprocessor macro that is only defined if compiling for OpenHarmony/HarmonyOS. + * + * \since This macro is available since SDL 3.6.0. + */ +#define SDL_PLATFORM_OPENHARMONY 1 +#undef SDL_PLATFORM_LINUX +#endif + #if defined(__unix__) || defined(__unix) || defined(unix) /** diff --git a/include/SDL3/SDL_system.h b/include/SDL3/SDL_system.h index fb58b71c42..6cdc10e1a0 100644 --- a/include/SDL3/SDL_system.h +++ b/include/SDL3/SDL_system.h @@ -522,7 +522,7 @@ extern SDL_DECLSPEC const char * SDLCALL SDL_GetAndroidExternalStoragePath(void) extern SDL_DECLSPEC const char * SDLCALL SDL_GetAndroidCachePath(void); /** - * Callback that presents a response from a SDL_RequestAndroidPermission call. + * Callback that presents SDL_RequestAndroidPermission() results. * * \param userdata an app-controlled pointer that is passed to the callback. * \param permission the Android-specific permission name that was requested. @@ -616,6 +616,99 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SendAndroidMessage(Uint32 command, int para #endif /* SDL_PLATFORM_ANDROID */ +#ifdef SDL_PLATFORM_OPENHARMONY +/** + * Query OpenHarmony API level of the current device. + * + * - API level 20: OpenHarmony 6.0.0 + * - API level 18: OpenHarmony 5.1.0 + * - API level 16: OpenHarmony 5.0.4 + * - API level 15: OpenHarmony 5.0.3 + * - API level 14: OpenHarmony 5.0.2 + * - API level 13: OpenHarmony 5.0.1 + * - API level 12: OpenHarmony 5.0.0 + * - API level 11: OpenHarmony 4.1.0 + * - API level 10: OpenHarmony 4.0.0 + * - API level 9: OpenHarmony 3.2.0 + * + * \returns the OpenHarmony API level. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.6.0. + */ +extern SDL_DECLSPEC int SDLCALL SDL_GetOpenHarmonySDKVersion(void); + +/** + * Get the path used for internal storage for this OpenHarmony application. + * + * This path is unique to your application and cannot be written to by other + * applications. + * + * Your internal storage path is typically: + * `/data/storage/el2/base/files`. + * + * \returns the path used for internal storage or NULL on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.6.0. + */ +extern SDL_DECLSPEC const char * SDLCALL SDL_GetOpenHarmonyInternalStoragePath(void); + +/** + * Callback that presents SDL_RequestOpenHarmonyPermission() results. + * + * \param userdata an app-controlled pointer that is passed to the callback. + * \param permission the OpenHarmony-specific permission name that was requested. + * \param granted true if permission is granted, false if denied. + * + * \since This datatype is available since SDL 3.6.0. + * + * \sa SDL_RequestOpenHarmonyPermission + */ +typedef void (SDLCALL *SDL_RequestOpenHarmonyPermissionCallback)(void *userdata, const char *permission, bool granted); + +/** + * Request permissions at runtime, asynchronously. + * + * You do not need to call this for built-in functionality of SDL; recording + * from a microphone or reading images from a camera, using standard SDL APIs, + * will manage permission requests for you. + * + * This function never blocks. Instead, the app-supplied callback will be + * called when a decision has been made. This callback may happen on a + * different thread, and possibly much later, as it might wait on a user to + * respond to a system dialog. If permission has already been granted for a + * specific entitlement, the callback will still fire, probably on the current + * thread and before this function returns. + * + * If the request submission fails, this function returns false and the + * callback will NOT be called, but this should only happen in catastrophic + * conditions, like memory running out. Normally there will be a yes or no to + * the request through the callback. + * + * For the `permission` parameter, choose a value from here: + * + * https://developer.huawei.com/consumer/en/doc/harmonyos-guides/app-permissions + * + * Strings should be in the form of "ohos.permission.PERMISSION_NAME". + * + * \param permission the permission to request. + * \param cb the callback to trigger when the request has a response. + * \param userdata an app-controlled pointer that is passed to the callback. + * \returns true if the request was submitted, false if there was an error + * submitting. The result of the request is only ever reported + * through the callback, not this return value. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.6.0. + */ +extern SDL_DECLSPEC bool SDLCALL SDL_RequestOpenHarmonyPermission(const char *permission, SDL_RequestOpenHarmonyPermissionCallback cb, void *userdata); + +#endif /* SDL_PLATFORM_OPENHARMONY */ + + /** * Query if the current device is a phone. * diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index b85d0275d7..83318e3764 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -1579,6 +1579,15 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_GetWindowParent(SDL_Window *window) * - `SDL_PROP_WINDOW_ANDROID_SURFACE_POINTER`: the EGLSurface associated with * the window * + * On OpenHarmony/HarmonyOS: + * + * - `SDL_PROP_WINDOW_OPENHARMONY_XCOMPONENT_POINTER`: the OH_NativeXComponent + * associated with the window + * - `SDL_PROP_WINDOW_OPENHARMONY_WINDOW_POINTER`: the OHNativeWindow associated + * with the window + * - `SDL_PROP_WINDOW_OPENHARMONY_SURFACE_POINTER`: the EGLSurface associated with + * the window + * * On iOS: * * - `SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER`: the `(__unsafe_unretained)` @@ -1706,6 +1715,9 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetWindowProperties(SDL_Window #define SDL_PROP_WINDOW_HDR_HEADROOM_FLOAT "SDL.window.HDR_headroom" #define SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER "SDL.window.android.window" #define SDL_PROP_WINDOW_ANDROID_SURFACE_POINTER "SDL.window.android.surface" +#define SDL_PROP_WINDOW_OPENHARMONY_XCOMPONENT_POINTER "SDL.window.openharmony.xcomponent" +#define SDL_PROP_WINDOW_OPENHARMONY_WINDOW_POINTER "SDL.window.openharmony.window" +#define SDL_PROP_WINDOW_OPENHARMONY_SURFACE_POINTER "SDL.window.openharmony.surface" #define SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER "SDL.window.uikit.window" #define SDL_PROP_WINDOW_UIKIT_METAL_VIEW_TAG_NUMBER "SDL.window.uikit.metal_view_tag" #define SDL_PROP_WINDOW_UIKIT_OPENGL_FRAMEBUFFER_NUMBER "SDL.window.uikit.opengl.framebuffer" diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake index 7c2f1162be..bd342ceb67 100644 --- a/include/build_config/SDL_build_config.h.cmake +++ b/include/build_config/SDL_build_config.h.cmake @@ -297,6 +297,7 @@ #cmakedefine SDL_AUDIO_DRIVER_NGAGE 1 #cmakedefine SDL_AUDIO_DRIVER_QNX 1 #cmakedefine SDL_AUDIO_DRIVER_DOS_SOUNDBLASTER 1 +#cmakedefine SDL_AUDIO_DRIVER_OHAUDIO 1 #cmakedefine SDL_AUDIO_DRIVER_PRIVATE 1 /* Enable various input drivers */ @@ -404,6 +405,7 @@ /* Enable various video drivers */ #cmakedefine SDL_VIDEO_DRIVER_ANDROID 1 +#cmakedefine SDL_VIDEO_DRIVER_OPENHARMONY 1 #cmakedefine SDL_VIDEO_DRIVER_COCOA 1 #cmakedefine SDL_VIDEO_DRIVER_DUMMY 1 #cmakedefine SDL_VIDEO_DRIVER_EMSCRIPTEN 1 @@ -513,6 +515,7 @@ #cmakedefine SDL_POWER_VITA 1 #cmakedefine SDL_POWER_PSP 1 #cmakedefine SDL_POWER_N3DS 1 +#cmakedefine SDL_POWER_OPENHARMONY 1 #cmakedefine SDL_POWER_PRIVATE 1 @@ -530,6 +533,7 @@ #cmakedefine SDL_FILESYSTEM_PS2 1 #cmakedefine SDL_FILESYSTEM_N3DS 1 #cmakedefine SDL_FILESYSTEM_DOS 1 +#cmakedefine SDL_FILESYSTEM_OPENHARMONY 1 #cmakedefine SDL_FILESYSTEM_PRIVATE 1 @@ -551,6 +555,7 @@ #cmakedefine SDL_CAMERA_DRIVER_V4L2 1 #cmakedefine SDL_CAMERA_DRIVER_COREMEDIA 1 #cmakedefine SDL_CAMERA_DRIVER_ANDROID 1 +#cmakedefine SDL_CAMERA_DRIVER_OPENHARMONY 1 #cmakedefine SDL_CAMERA_DRIVER_EMSCRIPTEN 1 #cmakedefine SDL_CAMERA_DRIVER_MEDIAFOUNDATION 1 #cmakedefine SDL_CAMERA_DRIVER_PIPEWIRE 1 diff --git a/openharmony-project/.gitignore b/openharmony-project/.gitignore new file mode 100644 index 0000000000..d2ff20141c --- /dev/null +++ b/openharmony-project/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/openharmony-project/AppScope/app.json5 b/openharmony-project/AppScope/app.json5 new file mode 100644 index 0000000000..6435fba585 --- /dev/null +++ b/openharmony-project/AppScope/app.json5 @@ -0,0 +1,11 @@ +{ + "app": { + "bundleName": "com.example.app", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "buildVersion": "1", + "icon": "$media:layered_image", + "label": "$string:app_name" + } +} diff --git a/openharmony-project/AppScope/resources/base/element/string.json b/openharmony-project/AppScope/resources/base/element/string.json new file mode 100644 index 0000000000..198f1e7b39 --- /dev/null +++ b/openharmony-project/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "app" + } + ] +} diff --git a/openharmony-project/AppScope/resources/base/media/background.png b/openharmony-project/AppScope/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f GIT binary patch literal 91942 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%0Ap0ZVyJ9bHJd-F5rWcr-6&wQUBt#A?D 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 U@9kJ6^9|$$Pgg&ebxsLQ0QH2fzyJUM literal 0 HcmV?d00001 diff --git a/openharmony-project/AppScope/resources/base/media/layered_image.json b/openharmony-project/AppScope/resources/base/media/layered_image.json new file mode 100644 index 0000000000..fb49920440 --- /dev/null +++ b/openharmony-project/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/openharmony-project/build-profile.json5 b/openharmony-project/build-profile.json5 new file mode 100644 index 0000000000..f5b4a7bf01 --- /dev/null +++ b/openharmony-project/build-profile.json5 @@ -0,0 +1,42 @@ +{ + "app": { + "products": [ + { + "name": "default", + "signingConfig": "default", + "targetSdkVersion": "5.0.4(16)", + "compatibleSdkVersion": "5.0.4(16)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "nativeCompiler": "BiSheng", + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/openharmony-project/code-linter.json5 b/openharmony-project/code-linter.json5 new file mode 100644 index 0000000000..073990fa45 --- /dev/null +++ b/openharmony-project/code-linter.json5 @@ -0,0 +1,32 @@ +{ + "files": [ + "**/*.ets" + ], + "ignore": [ + "**/src/ohosTest/**/*", + "**/src/test/**/*", + "**/src/mock/**/*", + "**/node_modules/**/*", + "**/oh_modules/**/*", + "**/build/**/*", + "**/.preview/**/*" + ], + "ruleSet": [ + "plugin:@performance/recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@security/no-unsafe-aes": "error", + "@security/no-unsafe-hash": "error", + "@security/no-unsafe-mac": "warn", + "@security/no-unsafe-dh": "error", + "@security/no-unsafe-dsa": "error", + "@security/no-unsafe-ecdsa": "error", + "@security/no-unsafe-rsa-encrypt": "error", + "@security/no-unsafe-rsa-sign": "error", + "@security/no-unsafe-rsa-key": "error", + "@security/no-unsafe-dsa-key": "error", + "@security/no-unsafe-dh-key": "error", + "@security/no-unsafe-3des": "error" + } +} \ No newline at end of file diff --git a/openharmony-project/entry/.gitignore b/openharmony-project/entry/.gitignore new file mode 100644 index 0000000000..e2713a2779 --- /dev/null +++ b/openharmony-project/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/openharmony-project/entry/build-profile.json5 b/openharmony-project/entry/build-profile.json5 new file mode 100644 index 0000000000..18b39a1080 --- /dev/null +++ b/openharmony-project/entry/build-profile.json5 @@ -0,0 +1,45 @@ +{ + "apiType": "stageMode", + "buildOption": { + "resOptions": { + "copyCodeResource": { + "enable": false + } + }, + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "-DCMAKE_PLATFORM_NO_VERSIONED_SONAME=1", // Otherwise, we load SDL3's data segment twice. :( + "cppFlags": "", + "abiFilters": [ + "arm64-v8a", + "x86_64" + ] + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + }, + "nativeLib": { + "debugSymbol": { + "strip": true, + "exclude": [] + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + ] +} \ No newline at end of file diff --git a/openharmony-project/entry/hvigorfile.ts b/openharmony-project/entry/hvigorfile.ts new file mode 100644 index 0000000000..b0e3a1ab98 --- /dev/null +++ b/openharmony-project/entry/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/openharmony-project/entry/obfuscation-rules.txt b/openharmony-project/entry/obfuscation-rules.txt new file mode 100644 index 0000000000..b78c7692ce --- /dev/null +++ b/openharmony-project/entry/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/en/doc/harmonyos-guides/source-obfuscation + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/openharmony-project/entry/oh-package-lock.json5 b/openharmony-project/entry/oh-package-lock.json5 new file mode 100644 index 0000000000..7c36300c32 --- /dev/null +++ b/openharmony-project/entry/oh-package-lock.json5 @@ -0,0 +1,19 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "libentry.so@src/main/cpp/types/libentry": "libentry.so@src/main/cpp/types/libentry" + }, + "packages": { + "libentry.so@src/main/cpp/types/libentry": { + "name": "libentry.so", + "version": "1.0.0", + "resolved": "src/main/cpp/types/libentry", + "registryType": "local" + } + } +} \ No newline at end of file diff --git a/openharmony-project/entry/oh-package.json5 b/openharmony-project/entry/oh-package.json5 new file mode 100644 index 0000000000..a423c238ae --- /dev/null +++ b/openharmony-project/entry/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + //"libSDL3.so": "file:./src/main/cpp/types/libSDL3", + } +} diff --git a/openharmony-project/entry/src/main/cpp/CMakeLists.txt b/openharmony-project/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..297e41036c --- /dev/null +++ b/openharmony-project/entry/src/main/cpp/CMakeLists.txt @@ -0,0 +1,18 @@ +# the minimum version of CMake. +cmake_minimum_required(VERSION 3.5.0) +project(loopwave) + +set(NATIVEAPP_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + +if(DEFINED PACKAGE_FIND_FILE) + include(${PACKAGE_FIND_FILE}) +endif() + +add_subdirectory("SDL") + +include_directories(${NATIVEAPP_ROOT_PATH} + ${NATIVEAPP_ROOT_PATH}/include + ${NATIVEAPP_ROOT_PATH}/SDL/include) + +add_library(main SHARED myapp.c) +target_link_libraries(main PUBLIC SDL3::SDL3) diff --git a/openharmony-project/entry/src/main/cpp/types/libSDL3/Index.d.ts b/openharmony-project/entry/src/main/cpp/types/libSDL3/Index.d.ts new file mode 100644 index 0000000000..7589256fa7 --- /dev/null +++ b/openharmony-project/entry/src/main/cpp/types/libSDL3/Index.d.ts @@ -0,0 +1 @@ +export const provideArkTSObjects: (ability: UIAbility, atManager: abilityAccessCtrl.AtManager, locale: intl.Locale) => void; diff --git a/openharmony-project/entry/src/main/cpp/types/libSDL3/oh-package.json5 b/openharmony-project/entry/src/main/cpp/types/libSDL3/oh-package.json5 new file mode 100644 index 0000000000..aa0d1e5908 --- /dev/null +++ b/openharmony-project/entry/src/main/cpp/types/libSDL3/oh-package.json5 @@ -0,0 +1,7 @@ +{ + "name": "libSDL3.so", + "types": "./Index.d.ts", + "version": "1.0.0", + "description": "SDL3 interfaces callable from ArkTS." +} + diff --git a/openharmony-project/entry/src/main/ets/entryability/EntryAbility.ets b/openharmony-project/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000..5c65a1e74a --- /dev/null +++ b/openharmony-project/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,28 @@ +import { abilityAccessCtrl, AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; +import { i18n } from '@kit.LocalizationKit'; +import { inputMethod } from '@kit.IMEKit'; +import pointer from "@ohos.multimodalInput.pointer" +import sdl from 'libSDL3.so'; + +const DOMAIN = 0x0000; + +export default class EntryAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + hilog.info(DOMAIN, 'SDL/STARTUP', '%{public}s', 'Handing control from ArkTS to native SDL.'); + try { + // This hands stuff to SDL's C code that it needs to deal with ArkTS interfaces. We are trying not to write scripting code, if we can + // handle it awkwardly in C through NAPI calls, so we have a _tiny_ surface area of risk where the script and the native code can go + // out of sync. This function call will add methods to `this`, such as onWindowStageCreate(), so we can drive everything from C and + // have almost no ArkTS code at all. + // Which is to say: if this function call fails, your app won't actually start. + // + // ONLY EVER ADD PARAMETERS TO THIS FUNCTION CALL. THE C CODE WILL ABORT IF IT DOESN'T GET THE EXACT NUMBER IT EXPECTS. + sdl.provideArkTSObjects(this, abilityAccessCtrl.createAtManager(), i18n.System, pointer.setPointerVisibleSync, inputMethod.getController()); + } catch (e) { + hilog.info(DOMAIN, 'SDL/STARTUP', '%{public}s', 'Exception thrown! App will not start.'); + } + } +} + diff --git a/openharmony-project/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/openharmony-project/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 0000000000..8e4de99282 --- /dev/null +++ b/openharmony-project/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,16 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +const DOMAIN = 0x0000; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(DOMAIN, 'testTag', 'onBackup ok'); + await Promise.resolve(); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + await Promise.resolve(); + } +} \ No newline at end of file diff --git a/openharmony-project/entry/src/main/ets/pages/Index.ets b/openharmony-project/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000000..af12664496 --- /dev/null +++ b/openharmony-project/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,15 @@ +const DOMAIN = 0x0000; + +@Entry +@Component +struct Index { + build() { + Row() { + Column() { + XComponent({ id: 'SDL_XComponent', type: XComponentType.SURFACE, libraryname: 'SDL3' }).id('SDL_XComponent') + } + .width('100%') + } + .height('100%') + } +} diff --git a/openharmony-project/entry/src/main/module.json5 b/openharmony-project/entry/src/main/module.json5 new file mode 100644 index 0000000000..ebc08bdcc7 --- /dev/null +++ b/openharmony-project/entry/src/main/module.json5 @@ -0,0 +1,67 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + // !!! FIXME: we can maybe put "car" and "wearable" in here, too, if SDL will run on them and we could test it at some point. + "deviceTypes": [ + "phone", + "tablet", + "2in1", + "tv" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + + // PERMISSIONS: Uncomment if your app needs any of these things. Please edit the "reason" text to more-clearly explain why you need access to the feature. + // Documentation on this section: https://developer.huawei.com/consumer/en/doc/harmonyos-guides/declare-permissions + // Lists of specific permissions: https://developer.huawei.com/consumer/en/doc/harmonyos-guides/app-permissions + // Note that READ_PASTEBOARD is a restricted permission, you need to take serious extra steps to use it. https://developer.huawei.com/consumer/en/doc/harmonyos-guides/restricted-permissions#ohospermissionread_pasteboard + "requestPermissions": [ + //{ "name": "ohos.permission.MICROPHONE", "reason": "$string:perm_reason_MICROPHONE", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } }, + //{ "name": "ohos.permission.CAMERA", "reason": "$string:perm_reason_CAMERA", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } }, + //{ "name": "ohos.permission.INTERNET", "reason": "$string:perm_reason_INTERNET", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } }, + //{ "name": "ohos.permission.READ_PASTEBOARD", "reason": "$string:perm_reason_READ_PASTEBOARD", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } }, + ], + + "abilities": [ + { + "name": "EntryAbility", + "orientation": "auto_rotation_restricted", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "ohos.want.action.home" + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "EntryBackupAbility", + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + } + ] + } +} \ No newline at end of file diff --git a/openharmony-project/entry/src/main/resources/base/element/color.json b/openharmony-project/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000000..3c712962da --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/openharmony-project/entry/src/main/resources/base/element/float.json b/openharmony-project/entry/src/main/resources/base/element/float.json new file mode 100644 index 0000000000..33ea22304f --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/element/float.json @@ -0,0 +1,8 @@ +{ + "float": [ + { + "name": "page_text_font_size", + "value": "50fp" + } + ] +} diff --git a/openharmony-project/entry/src/main/resources/base/element/string.json b/openharmony-project/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000..6e1a147355 --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/element/string.json @@ -0,0 +1,33 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "SDL Application" + }, + { + "name": "EntryAbility_desc", + "value": "SDL Application" + }, + { + "name": "EntryAbility_label", + "value": "SDL App" + }, + { + "name": "perm_reason_MICROPHONE", + "value": "Used to record audio" + }, + { + "name": "perm_reason_CAMERA", + "value": "Used to record video" + }, + { + "name": "perm_reason_INTERNET", + "value": "Used to download additional content" + }, + { + "name": "perm_reason_READ_PASTEBOARD", + "value": "Used to read from the pasteboard" + } + ] +} + diff --git a/openharmony-project/entry/src/main/resources/base/media/background.png b/openharmony-project/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f GIT binary patch literal 91942 zcmeAS@N?(olHy`uVBq!ia0y~yU||4Z4mJh`hI(1;W)QE~$uoq5gM*`nBc+3ZL4m>3 z#WAE}&YQh~)81J1>At-9nrFHCjnD7icYPN$kMC+Y%wh0EeYICw{6ojCB|B8T-f_?V z@vHv-{{R1ft12md+CS(0hb_N-Oy5 zueg`A6wByntx2xGd+V;{-7=5k@4Yq8{=a_RZMJmD5$&8MFIV=NnBO@rYxSsha#T<9 z+UnoSf}$8!RlT>kHu0L9g#W80tCR1h&HKOW-T&Zz9~TJtxBUS>gAxSrACPB&h`Puf znE(F^h!eNIcl!3s&(d3GYJLB8UTjZy`G@Y>XR`&T9G}R(Gh6rUS3ld8!tX!q|G)Y3 zyLw0YT-jjx(*G+Sr|oBuaICy@uKCV3j^mGKwfp8Z1k0XG-+y_2&9+#6yPa3R@B4NC z^$**f%k35=-+gxdS=H;;5x;vr|GX~u#s7uJ=l!4EH}`G$S9|?C?DC9^4>P|u3;vim$ugm?{0jJb@BS&rr&B6 zX`KDz3&YJ>@2(rRe$AisI&?$dUx7c#vdRDB-@jhkzqoXA(A%%N3Mtk*=CN;=tnRS7 zHh-7Zu|g?E1xRdtV)(=80P>%U%M->Q%ncA9o?tHEXAl(psqOQovi#=a@M!hlD{elo zzh)n)kiYr)cImkl;R~nEzVzYlvd35F_3V3eLv8ufXzqnCLOkBQ*ONWi{EJam^Quba zxyKc1|2~vSZRqt`ldR8sXSUny++$_g`wd=t%zGW10e+a^rdd~tzEK}o68bCqq^r`cO?ayR<8mpZ$fmpK3Iul^C5wLPyQ>F!az zv-fnZf?hmoOO_SS{iXO|@A*8$TrwFNTm&-vd7PRP=~=YGTfWyM?Od1v!$SW{=aA(B`oU z_uBKWynXE;&YgaoBN*3 zJ!Wbu8(wDkskFFj+wamlOQT{=*)4pW|LJ)go5|&kGMew^On(0{`)+67``1Tzz1+3a z*ZmI5BAas@3}Al=PGm9q`de@(u+nwjUj%Z&Vv~2lXnV=ckOAfxun*93fTdCB&;WA~h+d3I) zH}<7}W8hoeZW1E!tSW1+$I7j4R+lOdv>IR9xbN7$uBx4RdkSA~_|^ZS<67fv9fKoB z`w|5?8 z%TKRb@^HafPx)N^{0~#_FITQCf4Siw>rFd%kNH!c@1EDcCi%SlzqEqs-xRlApZ#f5 z(WQ;=i`7ps-@hh!>{I;5KkRQOOrQPTeMR*dt9fr7H}F+ISpC1DZ_2*B>ci&dpFN(( zZxy_C^S*hk?CQn8r2c&|yqUf*CvAg08;|ak|6a#8y?gfOH2-S`wb%0N_dhS4{`kOd z|ChzW%YTXI9$B?x?V-M9eIIUA9o^H9J#|Kby(gUcUG43q!p$53T*YsQQ^zw z556mIziBJoAGiK`xbauH)~UxoE@6oAQD@N&s!&-#0##+c&#= z)xVlm%3tQ2E(yv0Xul-+z2mRHrZfLrT$AQDcy@b?@iI;C)wbIKz{D4(q-N(?` z8{c^x&z?P7WbT^BPyH_xn*3jpykNC*#D#k?A&EBA7yV%h_;&HC;WcBdihgXAN2R@>oVOR%B$^`_E>%SW}{(|25oo>qE?h^FO$6cpWtH)vC_U|1VUmo!|{`FAo?LAfH=D#-g+2*QMPk(T=A@0$Y%=NPOn3AqO4>-QA zZGHH>_wPR)c5t83eR7}L_m>rai$5?;xE1O1;fhD)opU?yUlY8wu-0wT@&D^zczk_v zQt-tqi^`P_vX2*@P4|fker?x-wcJ6fBzCYjbeyoCP}`kxd(KTJ@mnj4!{)y;Huz}j z_l;d5x8y?Jm!3J^JKi^@^?xgEZo9R6&g>Q6^L|)cecN+-F{6t@{qJLuk^HGw_~twg z|CzWr`7Yo4^B*PW7e*hy*z3LQL)Aa^TQ#ihTE#1r? z)vqU&%YT=A@VocR&nExaM{}@Od>M=a6aP=UGxgls+s~H&dcREU&euZsEq_$D-qWd; zUgBY8^WR5SZik$Bkay|j_S|3Z+t@E#zI(n!{FLS8kMjcRyacz(mFDe?SXb|(F53O- z=kwRk0**UJJ3q0LJ-I8e{Xvn{_RU+9rY%~(u6mVnE8p`gW%ZHAbNAkVJ>&e!efgOc zmQ!{uS6;tz_wWC8vNz|MZfpp2zAMf-=Y;XBpfwx%baqyWcC<7KJT}JGEls`3`(Mbfkl!)u z-@ZBj{`J}T|5-~{DpyW_oG5>x@QT{9g-mPtg8#mq=Idu#TGsbkcJD{O=J+2W?&l6o zGgHjiYI)11&5hyj^0e7j&o<2~ufA^Z`*OAPlQ-}F?dzNUd&Nz)WvVhSBgA!+51N{q z-K;+uc`G;TN7dfA`!9d)Hj(1j>z;Oamejv5_7=zAZ7$qmb}V`QamTBERm&f#Q(uCv-oAQQ_IB!|whY@1@9f=Yn%=%Shk5Jeem;Bt#>EUzOj%~^*13JZ z_o>vWw%Q4OrEl)#-8pWhS39R|&T+GBSv_`7@mdTe(ua=uGV`Y{tW+^kidgEE$Zr9_zXP4RJu`gS= zJLK?{uGuCT3zTo>{Mh&MZg64E8s#O9v(Nm`wYuhd+H=v+!vD<}WLLTQzZh{!yi1oNPb$ zQuasnV~@|RYhUyDs(jS<3Az8aurt1x;l^e)k9$MWoVLPDmRoo3=9j;WD2;zF`wD;VzH3ie z{OcLPU-qv3_D#N4_H|lP*O}5^*Nt$t*!hzted~VZcWTA{%VmDb8`LE~sUDQTGSwx&TIp4}#lFh1&ee*|$`wNrff->9NI{R!VotJp_KjwHNdxfO*mYqw_ zrEjw_zm&M;V!)q>-&^DF2YjoW^4{8U-@QLy*S~vh`R>$?+su#Ozqs*w{-p*ry`Aqe z-naY+nEA4TZT(7N+3L3{s%}<6{~2uSAMP{7T3cUWQut)wQa2-Vf3N?Qrv}S5Z+KSm zMy2-tvgw=pi(i>-YAfiw@aBlsMZdS-rCvzLE>PO?FE(rG?`OAI;_GYrum4?N`{nP%=T?)}CKdShmc(SQbG5BL=J&r!M_T)A zQ7%h{uW#t=>-H}^{w`OVb^LwBq;T0}@v}eqlMS&31zN*Jet8*(ug#C;$LC&~8*y^l z7fa@Pt4N3b;M+YGcl96W$^QP$x@Ovj8T*!fTc@{gldsgeZG9<+SnF@~G&=5``>OkP z&ikbecGoNJEWMS+-|>hacV2g0o`eOy$3 zhdFs?e$wRW)y0n-&oQq!e)EgM`AZ3xm=)yM`TGL1Ypc;Bq#gF<&3{S zBiwi72hIK%aU+9qeQ$PP{Ot2Df9%VP3wFM{uJ6wyA@+c25AQa}l}PZf`#3xO6|dE& zIK_p#d)4=^^0wl;CpE)a_Or6+HQycoHuZh=)SGS}J8R(ythEEuXhlOcLx;!&){a+` z$_=Ik&OUMAnRidnu|=%AUpD%6hF8{qZ#O6Q73QpFT|e*r{9Q|wD?W(V*U9kneAAOneBZG2Rvv%HI|enKpRW&B9N18? z>hY0qo68gT+`V4dz9>-cM_m1i#|iQ6H|?aa+h@wXF23`&^1}7PR+|$$@>usJGUmPR z_jN>_-*Rte_J!{Kkkj*f)|T3@-(Sr57Ad(Vb9Jm-$rACM7ngaLhSojolK=hv zhS8j4+3ceK(>Im;`dWR&{9Pl%@8l(n2TPl+jt}DxlfCw5-`(uHX&g@!SSpt#2YX0G|M$zVf805DQfc0z$4_^x?VB6+>G;+cKF3p5D@Hrtt<`$_>csgOXZLI^U9$-4#qF=*D%~`ZI?ZZCN zm#=T^&nKoK^=JJ96KC%)Ge6ZamRg6_Pc|#Qd+W5Z=Y4lKfF~6Jrc=^+?{}b4F z-rqj^w|-sn{mD{%Jf@z$eeD0mWDQ-frag3#JgIP7y8KIF zz02`~n++>x=3g+(YW{v@W1mC#;q9fnt=iIF?)$rMUcJmY2lov|`ybqjSml^?G{Ww( zpm_VHp3;_X(b6^NC)z$2mR-nyD?Mp5`!Vi!8@ERNmsx*#M{NCVtG16O_kBHP$Jku_ z_&RrQ#CPsX|BjY@x^&@odgG<(ZrDR<3&V;3Tm~%vUf1r;w2r^vXYG9JnSAzt!zs(( zy*IJ?VdENaZzH30;ZmUpQ{CC-E55sbT=uJ#dCl?gp+K7XOZ(RxpF6DPad}!@dpK!{ za{JF>jwe!0vZoT{;sxfpD>kfAZuu1WLhISLq`48tf7$F~ob~_|B~M;ZhrS_vXGAek?!B1y3oA5J>)~owZZJOCP z-N@(So@r|*ex6-D@!iICeZsGKS|9Y6*u>P^FI2v_y)-gQT}@Rb_gdG&v#;B~dSGvn zAPw-mV^pXY-E`;g=lR*WHq&lb-hY&56S1>Wrmy%hZcX6q~0BC)?{)#{@diu`^^PachpOIRvyc!Z+UZa_m@ViZ(r@NZ?w<%xIKRcU(D~A zKb>DIGWr&q-DLiw^?Fm^``3Hp)?fcFKKJuqt7oQDf9=l>d^=aUYG3^N>)-3|E>K>( z^7ixm*zK#8f1WD;&YRo#AzZHEteruvc--;NsVvrw}yx|p;vOh-h_w$_B*_K`o8}``?df5#J3i2!3neI4+5GJTilEg7UY8f8O7``Z(ZPob_ap+ADrGYvyLMEtqh7 z``l?${SPZz+PvbEwR9EfP5XO3oWnA9_S^P#!4?0^bb~JHw7U1{S#TNqCocQC{Odl& z`L6Nr*E}{ox50kf>xxe==e>&P?=`*iqIOO4PsJAXWo=Hz*LR8N+08Kbh_|@>@vp`I zmrJa2MCR3Z_wDy$4y!w0aQW*Bi{&MEZhoDA>*uT9+}XE!FLfR)yel$uH{;z!%4-Zo zKFQubSz2v%Ze6djY%$9x(-Ny|d!yVR6+SGEKK^e{YpXKfx<4~y)j!CzOH>qvKG^$7 zT6T8oluf+BCdWQHJo&S0S<(OO*X_fU^A<0d zuU(nHYx;isg^R^tLru>gVjX#JsAlk3@GaZy?Zf?>9LjB9NNXEzvHYMO^nbb7pDP#C zwZ7LLy>WYC-{vbpfBk-2`JF3NU3E=>%4#br7?Fm*1k7cdh7JR3swIvTGvcNW{qZtTDNo42Cj_IKqk z{Zju-zI@ve(|>H5nMTF;=eN_|U6^RvufLae>FHa__u~komssc*IV< z-C^D5H-1%dzMI`$=)3m(M`5dHRZEoD)Gtm?uJx~VIljlyp>m2KwrPn5v4;N(=d_(O zkNQ_WJ0SbgeJP>hV2-LaxAW44!z!h2-RCoYHX)p=_&xtRK7ZL1e$OwZZ`!{W&A96o zef<6nW53cPzn_&9ly=P$X}aDWb9b5I>R;7+e_5&VDz33GXZifUF{Hcvr9||~>c_A5 zR`;iBy9UpW<#;7)<@d+&_)4+YyJY5_|J?Td#`oV<8{fTa^%D(Pb9}4#5Pd8e& zy0XmSOUa$qnq;Xn-!=l@ehVjm1anFVOr?_lkEYk}TW-!+?+cjtWG5xYwEkqP$|mCM&}DT?y_Ie1+} z*W!5Xd$YIv1vUSqh3`H3Y;|Sg<#UCxKFzIjoo*fp@5=0W#rJZXTKr3w$4mV$OuRkk zPIqm+L6x|>mD&1@7BU&kcQ&q6{nTAv`s7O2{s`{da({04z29}Rl;8HjBJY;M0`12+^=-zoCr{5djWOQ1F8AB>#cvNb z7vK4M{oCBfts=ZZPx$@j#{6q~tt-p=SKnFjebS5nX0q#Z`HmH@dYqZ}@h-zGyZ13c z_buLVs{bvwitD;C|JLtd{R07J7e4Zp{a(7qDYNv>6PfpyH|D9E?ArD@Q`X$P zCi?glzkL11AGL>ZG$ao+T-Fhg*(IIjG{w_=Zupz}Nm=iv_%F%dt-EIB+(qx@WZ!#D zobhY9htizYvk(0VT(flI!5eG|Qj69d`zw%hP0*?Q+ga4dQeXaHE4RzN( z{CLzlb(B@+ZvCy!&@a%~$uM%%}Np`f&bTdpS^i znb_=WcjrF;BPQ#A*>A5|ag3bkDTS>`-`oQaFG;@ZXIK5n?>H-mRMsntPk-(&d;E06 z4OWZXlfv^(dmMlLz=n=f*% zNWRb96I){5QMvcWzxMRQ;xYB!-*3+~ty{1B-q9fO0-v4q`<)RH$Mcjw%A5TEc;U2+ zv-R@xXRmATS>2bnDR2Gk&Rx#OpY8r}+pIE}V@3Jg|MS|`U(YXD<9$+o-+rdN9pR6x z&z1J&pXxjp`J=9w{YzGw?D}24zh2Bwt~Y)6J)Z^JT+4_3mzX}x-tez%%RL3F+zG9V z?%Ov!G;IE-$bH_jdBdZLeHC|1dD)I#m3i%v>EIWv@+cxWa4uWRr2{Fm9#%DX_q>%9 z@Ty;>yeuq*`^fRqJ2Cm&U;NOnzwo~F2*;0JR@UOM*wqvJUVhO}{_WazBu~S9`N37s zO$yg2MH*AMk%AI?jYbq);7=U%GcQ#$*)#H&of>NBvl02W?? zqPrUWV`Ve@wr5(rzaaSRN4REY;Bn(>ziSiEEnfckpJaS-d*7Oed4HErUlYE*=f;=% z_CC!uzjy1+etDzX{IO|i@xS9%*HSm@o1XpdZ_#h_|4RKIkGFeHZ7P#G^H#j?7E_Z| z(tmGd@0r+ZJw(~n@PA`9!vx#(Ntut@veFN)%otZ!e9F$?5yt2v#*%6C}WB8R_^15 z*|T4p9IJfO`_wu%Z!7!c(w2pM*WQ-zHVFB9e_?Xe<$qm$)78}EKJ5D4+gHo~U*Cb?nvt9A4B|oZ9$6Cehe*T;HyU>D- zroa8y-dXnelE<4TLDSd-q_U>XwUah)sP%W5 z3^;c#Pky=MzDq*c;WO$!n-WTPpGe-R&9djZ=J{kG7hM0(!-$p-Jm zzZ}eb5Oz)bkI!+7^`W`?idS#Hem?6UhdZS$_VH{+PzzdomIvSJtisIv6>pc+r;G3Uwe&=~ z<%gS-?8Vl9G?txgU%WHld|v*Jle^^(@Z`w_Y?~B(<%-ti@(F#f`+pym%h+?w^LW(% zC)btvGm0nrr`z8WUbk-TTwuAisNU8gLX zX7i(;AVj5yvh3lo=o^G+dAup)@3OA6S0r!`H=sl4hKJ zfBVi=TV3THXB{ivB_6n@e(A&6XR}M*L@Vq{{ANAZ&-u&F^Sevd9Oluq$lz^UlgqpQ zy5I5l+rQSmx|TZmL&2Kcdydta-rxShdF|TQt%{^p_-fxb=wFfie&0c}_=jGuvcLJyElv5vw$**>r7jgbIyiSt{S>yopwo^TmX@9$<7Aff|8D79y!`(Y)<0GK{FjcmN|);% ze*G}Oo#SqQil}~6Z^@pz1z)EhH2A+XPDA=-g{|Jd#mkkKl4N&nB!w#h%NfAQQLY_Pp)epdhU?YBPF;Pzf7-y*+pe=kn{s^2U6$+EoErp~ObY|;7BneH{_UCp~o*ysJf{#s6U^XiHX>zU6c2fLrT z*q8lk-Mfd@e7|;SUblGr+3>{0xU1FS({KG;^qHalU&`O;t{>ihm3MOLXMIt+o|Qg{ z{hzT_TbaY`*Ou?D-#3=MUffbETle_<`&Sk_^4zQrsmZ*)=KI&ImjTS9e}D=9$ISc>9uypmN>C0v|3MHf2f^X1a1! z{dzOQ#m2sDZ2`Gkc~3=yj(jZL-S&j{Q1SE+X9AcMPpq7qts=a11+#3V^Cp=ipE~>Y z^Yfpro%hnitiQHvOP+@1mdSd49ILgf@4x8ydzfhhccY4|;+g%kUm4xsE-d@nXw$iX z2>aI(hn>3{k1aB|wpFX$Vco|X-(NB) zb3R$PRATMQuQ7Y2V?D3S8F(D7T%{bXUiK%>z3%vV|94y8ZG91NJearMYUS^SjfwM9 zUhncg{`6pAzb@gmISNY||FAG?YhFveZ~1fa?-E@F7TIm5oi5rQ7hm$?x9nE#iEHof z{dqsLc-`Zx&2fBhkFI+z`=BxBeZRn7@5#0TyOP(QFSK83{-VSDYaYY>{C{1_r{_0S z%iel!s}VN4w7$M&b)Sv-?9ch@EGO5B8C<=%uy2N);j*&T?$eHMzG8d1@X2etYa4C1 zOij5fyT*RXs}~0Ka<3&`P1}3@Ygy^%jp&S*x#orEPi9e`u#I&Hu%{E39A z3*EnG{@eZ6Yn7ea@rSl-{U7f_5sc7YvZ`jA{?PBf6_j?wdT*mRT#mf3o>{=y0*rtX+BU{_kG1ecAaA z<=t(4jxo=>p9>!TygT!NS%HSPzRC>KJ7>QOotE!f_nBYugfDOT+c~c+HY(g%u6#!N zsDiU~rIaGeURmqoiLq;x?>=5|HoVr)F!{7l+Wg{UG5=U(HEr^%^}m&zdHD0It9*LJ z&n-LUou1bJ=d_YLfBUtX>}1wO`<5F_$zPIee#Z1${p7y829I~#=_zUX89%Q#amfa= zGbJ_l({vY`e$_csI@j&eg!wC9Pc^!@(XQ{=TVboZ!>0vfgQKUNT>1ONwnf>t@BVAs z3f$Z7{8jCxu>C^ibt`B8{MXZWoq4g@_t!VB7KE3lIqukJ?vlIj-j!eBiZ49G1iU6n zyR((1i?uB4d--vH%$0@zcg+lzy)4tO!?EP??x>wp!hS@gVeP~qErnBf%UF=LG=2X6 z$i>3yhF0zun%wJN{+MZVU~loAbFP=yb+1*aw#dC8V1IAHV+CG`*AefA&u&-jm3jUphHr;M*Uee}aP`6| zrj`uLq@PJT2Pm#FZw|lpv2Iu$=?Qze1@a}qHdgFtzS)ctEt`^R9vx8I6Zs+sICj-5w}q zam{d_cKBDbo_D?Lx7X)cFEuH!`0z`ny!86Q(({H{!nYSHJGyH`ubch0sJKF2a56Xh zv;EcmA7@Fu{~z$RM0WYl_Umf1{_mdGn|*ZaGJ@Sf^Gt?=!u3CA@8NkfC*l0ltXz&b zuTMAi|83b-{%-yIr}r%)lh?ofC~$YzPPrdhk2iYXoO#~*{QslZ=hWD4RNH#_OJ(!1 zjsGu~WKAs2eP!ogddA&%-Nw^}?N({;zgf-Ny1%opH1f9a?u{|`7ClY~T)4#~^6|SH z85Un>Nxh!;>csD?4?njDPfIh-zNzN;Jt9t~+G>(BdN{=&lZ;`HrD6lK?cb&CAl{n+ntX(zMn57e->w z$`f=LSX4K>XkX5k^jyth26Ov^;KilZ5?1Dx$34#-c>H*sqQT0AA2^v~_3kdcA=!{r zep;{1s_d|$j8--|Fs5n0wN9-YbuG-)sASd<;3Btz6=tdu__QrCwIYinH2G|CM-J zC8=M@*nC#}b%*%t@W{8fPUW6=d$r{Mjf%y|vg>N3=AF8;SlRVlaz&QZIrS19smj}) z##2Psh-mhF*LwLRPG-*I2b}Eg&lY?xve_YaJ^$75?5wPgXCE)f2^{-8|9W9T--kPg zj4rK`{`9}=`HgD7|Cc>?_kZ27>!D|aZKlj`hiZW-8~RN5nt!Y><$C?hY zULn&HyX*|!zj^-um8z2LLF;Xvrk8Z9FTebH;9p_JJr&vCZsEJ~#bmdCw|*HRUvBk3 zqI^;E_44Xn&)r@$U#d(kPq@!g-QPFa?ETLEX=MrVjd%Zklazg({VV49tdhRTx*Q1~ z-@UT%6r2`OvSxMZW!C*K8SK|BkH~h_JalPR)YaPstCX{6SVh|3Uzq&nf5h>FZro4q z$}VyI-IT8Lnxok6CNtZ${aHF6cU+uhmC+ve=9g8TS>g`I<0koMtAE{nzN+8ny^Q(k z_qJB^>Q;KSPYP?cWBXj+-nXOJY2JqqAK7I2uV3-BW|{x^8!Lm9UVm1-c^^B$!TTSR z|Fha{UTs@2p=?6%GI#%DOL>lodH?o{`7iV>J&+*xIyR42~Ip=-$~gke?6lG~ z)ef>Jt$q9DKP~^~UeYq%xWeqxuBjz4{zhd&>?N8nU+#$hE5853?6`Yde^#xHS{j%< zoB74x=D0a8pC)|ix_RZJxVqXzf{w_Cu*{{ z@9zKbuHn6JwfCdnWd>*D&Ka&RFMYq<>RI)!^W{GkOC|Z1q$_W6v|41lD*s>v-)sAS z{iSnk%&;#SKw4t_=j6Flx0-j}$~nO-%fEWBvv57f*Z8m=dGo*XcZKh}U|W2R@0~cq zPSNCDMxPFx3G$uTceZ%Wk6^?4ls^&G4eDwOw)Gj9d|7wwalr8l)<1%Gzx;jS^Dn8G zmH&H7eO6veTkUi6oUGL%d(*FucDsM==smspZoS=?GxK+xd3@mW4DVf5*LJhM`KI7F zUvW#bU9pF}tKI&anH{9HBrvK!BUh?-p_POle z-YqCKF0F6w`|_t>7yDq_pPNfncW-L3f_+3Y;;o3Dsw|J$Z!e%&qlc#7D{^AmGK7u)DusJ||^d23e5 z=gyu>0)JU$r!Q_=o4C`=lI6XaZ05a5Im@k>CYyvvq`6s5Vr{;$rr!IkL|T>Zn$jmC zy}Q4DzoqfgBm9-xnaY=aIik$63;#bjpK<+Y`qA$W?i)le{GAY$b8F5gkA!;x$8Xra zxWRY#{?V|z#$vCDE6|IzV9yzo+}h6 zs$Z3y_o?W9&imWtB}Jc4G|SD_}Tr> z9g*Mj%6`V|j6<2PB>J8Civsg)uH{=CUpjeTxn<$2&-P#0?=OA)u=cZ{Z07q0&tGSP zWlugoFWt>kvZR#foZ;Pn&DpKb3O64Amo%R->i7f8w$)m;{~uTutx4Xs^u;W>nEwfC zD*OeZ@4w!9`9te;(C+oWZgFtCYgk>~7^(kF(ErgJ(e~PVPww8c`CrK2mTh~dxpB|x z+0W14%J|!V_cxUnlgDZxibyr*0r`>!fA1nL1(EX~0jg7OF+2P5F|F$ch z5J=kM)>hv|0AXL1zfpe*vqqJ<_nMG z4@>@}%6^{p>+84ovLa1${P&mJF4UzUD(Y+u?lr{99!}hl_wD+Xf2&SC{8<~}zQX&~67hz{*as2ErK<8&eg+=@ z?%etIQL=KM?$qyxj($${Ie#PDwC>;CWyweLFMYT={k~ORgn2pNp1c|MFD-U1_ut>! z8z;TLYkxeVpTmSSxQ04t$$h-;<9(Ic4EzRD*4~?XD}3Hx!O6R>+ndL2%f7j_b1wV3 z%o3@kpP7^S-!3({&AjN{9{JMgl~_uu!fLDL1kiUWz0Zq9Yj)+#NWZ1zC*oZKJp z+3i++dWBaCH~ed~|C>E`$Az?vW$zNdx-UNeMf~-BUMs(O|5hl=xZis6c-F0!H2=~! zSwE_HOOLn&T<>De?JHeVyDsyNjbfq0bhGMtzptccO}cjF*5sbwI!DqTs90{z`7d+Z z>R9%hD1F|(5}w)#?0fz{+Ow^4cHi}`jW1T~%zm+>hxJ?EgI!zA)?c@^dSu=;xBowz zZ2$EuSL$O~Zacpfo73w5!eYj@!}-T%2e(|GRxtx>E_c|*u;p;;Z^aYYA>8ba1*f)n zS440zu1t1c@qfyu*Q+C^FIs4@^`hE~#j~rjrYw3DHQl*DOg6d1C)k}&wr#@y?Qi2= ze{=e)pJk14!>0ch{UT`mc7X7q4t;o9(Q;nTrl z3)b}&^vzhL&ntE+;JB4t@1?kZA+5W-yH*v*_%Q4Ywp;l4ND zD!KS>hvKp+Sv|j^At9eUBaP&h7KQdXn2=y>FdayHSnVwZoRz zH}akDmwC-mnt9V_;csQx(~Hjj&acHD#z^~T8}_g}7^lX3Q&?|0ebdpVkm*ZzWfmlq zFuXTf$|3!8&qJFHYgYa6*H3h~-sW^QZTExhQ~M5e_B}DKjo1G!$=V-x@93G(@Ae

?XiPA>enPH(?5NLw2r{^(!8#y61VHPIX+cRSLV*2vGu&o z`W59bBg`bXG(L_ysNDCg^peCYKDplIR(#tOZtto4SjU)cy@0nM`tIvpQ=iHANx!@y z>*G7e8SBs-${@VOb@Q1HNx`=D6>WRgtl!jgo72wRBJXu)?zfBIcNW%XulrtSeLtZj zs^m_DLFDm!f9AjMKVbL&cL~ddj4OB28H@LB=Il9_Y&mI7^#$i$%h`XGe|J}izU=ou zIwWQDjb(Yk$1D09%ct&A=YO*7)v|}D3v=UZWxO+%ul#@HdJm7zgB|6(Ygc-@e-P4d zKlqQ~&w$H*M^*|#n=v*3N(%v*nc9)DkYXX&iy*)~@<^2r_%SduHXDoJPk z^~Fc}n_+7KUU&bh|N5+6{?k=%RVP{VmHVzSH}}Q+%JQ#eo>P`_xIFlH$<3H^nak^o z|5;AiDu3;`)v;ZB3(me@AN7CEtu6I^D?+VVOxH;-l)UmWsAb`rzd!G{sg$42=wtuZ z5bFL)nU~>Ki)rZvzShch4}Wf1*0!j``dV#C>6x#uF5KlfXK3@dE6aZV`srWPcwg=8 z`&t`(t=hO@U*E@@JLaU^Jg(RCn@`s8>ism|#RlKAci4S1++lWMa*KN8`Hy;6zgo?U za`*XKmLdDV|Kkp0So-g4!N1Km~sL)N{1H06^`o8SF^KQGsQ zIq>z?%pWzHTT_|#+$rhvo?p^;o6)3HrTH7nt@Vw+T;|tbKK`d_adM>byMv4k>=owz z-(N(;*Uw(H=RDiT*t!FJnpd@7OYDCC(&OdsdnUW;{wzA=-M!efw4l#?iN@XXpgNAY zV=ow6pI>uvuL`^PtJ~_D+=H2Y8_di~?<{q+y)MYJY5Jx;PY?aIDl`2fdH$wlQkL^A z=Jd_>f&1*DV)q4g-48n}`#1J|+?C1hk$X8LGnh-y54Y*ujh!P|^~x80NWdu#N1SLpOy`Ty6AUgZ}&4j?>J{IB5D zrhVs2H$I=YjHTQ6n27H5{gdsR|Evsu|L0ivE&Kny(?9*aa4+JV#(bBY_CG7*-@o2_ z?emRg`n#6-NWHjmHEnk5;?>F(&(og!%l#?J_<4(={@0_sXDa1S+x3^)_yqnm-uF7< z!7YyC6YXc%6s}Odv)ZyY#Q(TWuCVOF`cKQtu1~peE;GwD=5d1W-K%e3d1x;BmU-Zs z+-m=de4e5&OYXhDP`JgO^Ni-Ji0fy+=UzOv+wOl;UueC3=z6PVr}AGf-PN9*rM2JN z{S$k=!|}_erqjR2ZZOHepMCV}fyTbn`i<{0d~L0dZ+U+H(~TGRw|gE>UGew%|H7^_ z2V|`-&6@SQyR^EtrzGdxOaIo_^V_WsZF0=aPqwc7+x=g|A~v|ZG_lL@a_aV+EAlUH z{P+6mc6?XS_lph7E6#^MuK9WSyIaO_>?0}st?zn+rtF>dQSkPLYgX?bb3c+XD9O|8 zRyCZ$;^y{IaeA4W=C2By61l$uKG!zBdieF+`hr}MTQ9R_yx!DT%73~rm-qhGlFVX( zrn`Ixy%%~f^nbBeH3aoC*nnjt_)!V+Q*?VwjF<_hvS8Z)XN*M>&thw_jQy#d%9y8_TJSG#Hk%WinE>_5UHK~rS{3v;}Zja z^;q${d~sMXU9wj$U|NptU9-a*rz~o|bx=n1P2(PScdNd?JHNl!@wqZ?^|e}=*FU;{ z?q58I2VgO1b}j@Fk2P5q{SIk3L1IF#qh^2wY2dY;-5U2^B=<98PXZ8EImwO{?X zdwnC*^WV#ZHI!x7%b(cev21^5U;Fp7=69smH}*A`3SZZ~VE9he>HPi6zn=+PrE$Lg zXTMN+{mR0W6~F zU#ki));wHr@Rx;`y3qNT)dEb{c^l_%Xwxg=5ooY~o2j!UHXJTZ*<29kb?7hOb>_Kmk7yZs zZgtnyw{p5?>G$;-|Ia^j`%+goe+riU{aVe=J>dA|)yn7WW3GxcSzX%5*ZB1K#g~@f zPFh`i-m$om{rIdl9a+z{PB$04x*9g+-DF1&vpdP%eJgL-KEF}>d{Ic(?yVo}*C}6h zm0j;EGi`6(V&wwf>|-^lyTA8`$FeQF?ES>Vc2<#ksx?ohgq_^I{ARs06a8eLaI4Bc z{A+l&zu)td;Hw#m~R_BnQwfZxwy1w!+M6Bb0+mw?s)&Z zQaY#St+3TIv)8ZHnlum3vf6g}>)yAES10GsJ@xnc_mDSFLyo_3Y%ygm{quJoxBcDg z*{3q4{Bqi_U77pOwVd;c;3>0ToBK*{&RV2X=6LMZFCG8VIZOGwN?#lKn6f`V5qIo& z>6!A9x3AM=e?Rwl{H(faQS#OM*)#k1tmRmX`}R)mxr=?93vvc_?&LUmTJ|nWe1cr$XNLJd zqk^pyE*z}c_++k!D|0j7)LuWyJzq}WpWtBR@LOv`-_;Y#tp5Ezv6lS@pX|x&9w(1F z|2LMc{vNo-^wP$cpWkLl^xwXG#ZcsuWSxt9*5?m#J%5i|{?oGGGi|a<{Oi^0lb@7? z&i!#lQ}^TdEIHn}4eaHUr(2a2vDuiXS;hTdbNi&X)Li8sd)PnRll@vNo%#EWyH!(p zg;TJ*4A+mz$=1uQ{=E{pZFT6g+TZ4DS1cOtzwVfResG+3+i?66L*(&; z#$wm6%r1GuoBPDk@4EF#$MeVjcl4!-H&&O#+>c)P|E!$E{0oATKfdmMt@Um$L+a#> zz6<`{-!mcp-`=f_P}?_$Eq$$L$>A7-lI$tB@)tZ-nEU8sVa2bzul41^ z{qJ}jFMpgX({5h6<`=6~+#j^U3 z`!05FShn%3X4(DS{~`~YG+a2oRqIT$-O2C0dn{~BooeqSZNAwWqi=O>;g%K3cOKvV za74Of$#1o_nerDVUM|1-I=98S^7r{=tNUi$dcXCe;I8HA#>d_r^6Z=E8!r3u*X@V` zjTkBajVqJueg*63zl_*!v@!I~vSia49|W&23%{~4Z~FU>zjo9fD86f7)0>dt+xoHR zjp6l;fs6Nl&@0*RT66R_+x^?lu^O{q{@8cX!pw$m-@m2D*<{Ns{{Q(Wn*CkPGCnA= zx6$(7t9}13t&Wsvz&bF3+-bMJdCE=h%B_u#$7>tct5+S`75C|hhff>m)w(yv)gPGY~O!7$Grc=kJ4q2Q{vkv_Pr=xdwycg(c@N&W^P;g@by&+QW~PUZjR@cdTj!(GeTHr?QP3^);ne=UAeuKkT-v z-{gL2AJ_X+uiGBoIK@A^R#hqEM$P|n8NPKZcfa29-1zy?jo%))zlfasJ$=VN$@3qB zu9>pz|GmSf-sbVUJ4c)<@*BRY+aF(-dL{8Lb6oz$hX*|`RUf`y#oV%yx8%*2APt0T&Zm3nbe{9X;rM*A?D89E^QyBk2&eqcP(}MnOGaGmPRZTZ$ zyc@o+L}G7${J(oI`e#1NY;IVZ_cu>ww>kENJ=xh{-NQqr8iiI|PLm8-3a6Y~&)5HE z+e~HCR}ox(hr_oUdCnJp&V2ak{(IM}Z|7aLKUR8oiE_)<_rF&k)(Lcfv48eH-M+kK zk4rVSnrS}y$!xXF^v_C}S&FjVRV&X+%)iFXf4?B(xT)1Qh^Uwo__kVG(DN+ zUG%H>0muJzUGJCux2$c$G=!=P;EgZe7jG?l z{??N6Il;0Uxoeg`N)HV`?pR{g;m}u-r0U+&v|DgWw zv2-ungQu~pzScfnToQEPl`QM|)KBYNPpq%_Tb<;zvVU={=ne0n*$#c*-919pt;~Mk z@QF6RD!05?x?+2>`1}hWqw=3S?n<7|?Gd{}#b?1C#S+|~1( z4A$2=7+X$SBK~ZfxBS;u&Fpp0<$pyiISc{LvO+Io?d~A&@R9fNZupmA#Nm4?>1>=;d(Pb-b)T>AHrttF`+wi`8GjePf4z6L zcl!NrHz!_HeY;cOmE7weU)NpRueJRDmD2U@Uu(;r%w1@fW>vTMxA9fg*_W^F_K`bL zW+(pP!KJTqrvi@$cti^9{>^;k)6BlW>)AES!ieO{_3_TTKs zn-jawe7|v^*{0I0ab|vG@cT~-{LidTaQf35Z!Plg_xY5ZegAHLUoijG40-##reEJa zU*vw}V~KBV^uiU^AKHVuTs}pnH&?yiU}6@veZ}vHw)Ya=N5g)DmYDxasLD;`eazrw zIOlPAp#HU^>UWMcpR>Al{NQ5CJ6mUeT`$8UelS^?ui^K8NB2APwattxWyJp;?a#b5 z>(-wC?yEDZkJ#yu+)HC7+yv9z(@R>T|Im-z1Pk2Q>jB`iq`G`EUI83b9UewW#Og;#b@ zr)SpsHOYThu2qgyFe?q_+Mi^yBsu)>{CDS*E^O+%F7d$ksoILi^;fN)X})>gUpVjV z99_*?`I7Qgk7p}v*`9n;*8K9v9M-(`D@~?dP=C!)IB$2NdFhhJ6~0GVpC7fl_W0$z zq+QD|H96Oolr9M^SzKSfR%Xrq`z5(clHXt0XqT7n_-eT^@7?wPCKToDe#(DdP@&rG zg#u5)CHM8*Ja_Nk4UR7j3_pHZq;`Gy6+^MO<3VB{E%WA`ee3>pVc+6#MvLD6OtViv zxo8okz5Dh12OG~?1zDFmm!JLUyS{Ko-r?*RJ9n>(<=1Sl+g>UB-M4+qXGYnFyI*E@ z{9o7q`tOYCO|M_zIec2J z`?&MvkyTY2=jPs|4k^!qvgFr_WZP|* zf8MXkn;jQ)$?*4Pwzw>dJWJ;bg5UWR>Vtl`$QAfpoN=t!RCY-QtQR`qSIUd1Zy2R}D$t2mwU+YB| z+-2RD{5W~G&i{v>clLd>jJp18@978CE|)gS#YP_wxX8hP<;X+ia$tUr@yn-G9DD*OVKADTF2cEA7Cqf!6n-1AM_EH7>>?>qUN zvsqa-b2rncpU)RImgg^0mgqh4tmEEA{udS>8IsSX$;jN9JtAfAW=f8fo zYhf+lE~`c5a+A5;Wgpvo`}Vw}W_>fa?7UYIvp=X+mfJ5(wmVvtGyj6&+Lc%AXE?mH z>r?lv)wy-VFW`8{&)mJS|E=#=UOk%fZ~wJcZ4=IFg#(tG`d$Z4*(_DNG}B*YMtzOh zp;~JL)!_fHm)`o`;QwrYZt=W5$727pozBghey_Lc@7q_GbltusyeipbD|o5?&*Dwa&sm9F{!`%rq0#QB%Mq;G55 z?`i657B9cGph54NiAB}%M!jop)BJH&U^9-JvTW^ZTww6uQ_6x*eBpyui%pw9`1p9c zJp8rTV)Z}EeFiM-hu$<=O~^I=uPnQoUvZ7VG0ER4vf7DxHtD|ajZ3GOMWGcQds=NNY+OM=nitu>TWSfWA zO?akcG5=Lm7A{$F`*Xwcv|0Cjy(WsBzQ}AZdD}?D%p>t`xb3#BVPCFZDelVL;bwL1 z-cQdc125;cT(jWXr8{4q{_&`!>Ut%&GjsC=^^!B0JFbOID6`U>^*=mv+HUC=9WSp; z^DnL6{qz63t7lh+Rn-RFntlDXnymd)w!TX{P2bnooNL;(#9;ah6$>8!)L+dny8f!F%FnPyTS_VuYr{T0P6_f_*%x5?1Dy1f`;)*Ye*vdd_ji*Q$^` zmapHXTb0e!h-`oU*S$n1x_8dp;1nUrg;gkWm#63d)IwG6*;LSXP->&{-D_FGyZ+o-*)qwZ;g|CR?(C;+bLi7 zALrS}AouFTw~im3_ijIwQT`&kTRY{&eS+nrypQu&(OSM(evkVRo4mP2v*NXnwVm2@ z)vCOoea;-2$yf4j3h`trOfv~6wa)ar$XxR5si&Rzm-f4p*`My8-nVjcy}Y^S{IB69 zEb*6B=7xm3-EVRFha0k2Th`wBYn8NW`<6RjUSE1#zSrVa@ey13pxx;JRz8UT^c~C0wI(ZJn8o zNqgL4x758`-l(wKiO+H^bBYF28t3_L#pRmQmgJeR*#^Cg3TfY(BD;|L#k#rsw0LLx zeG0a^Hs`fRuT#Y<^^!R)et{eryUb2BGyE-1NPqG7yOp1gMWp+#GSA~`C$9-PXY!ZE zNU!a`z2sobv*e;l5=`Gqe{ws$i~qRh*R|fST!&{1-d3Du_hRSN$62=@M_ODljQw9R z>2LMZTj_CIUm0y{xnlbM^2BF9)MR(Rx6v$`SJG1UZR>q`w$Fcq|5VGe`)|3!H;wPz z)K44E8ZY^MF4$VZ&DZb6D~rx^YO{|XXxuPGHg4H(zg=%1aDL`>I6r^NwD$~Ece`xo zz4Fj}rD5}Dxy(JoLIaU>`^@K2Y~hk$9AAfB`|yUBVeRe<&hxfeovBZ{t57YoHyC?B zB8}I1R5R3+e@+l!e(&_{QzLukNA;`61No!*4QAX7oBdmD?$NDUOT%U^k;-1?al~%h zEe9#Srb___Z=_jWvlG{Pz3sn$>#qOH`sUoqG+$Z%;>Lcn;_vNNX6FmvGumI>_}uaE z@4sJnxgRgNe=YTeBO8C&zJDw0Bh1+|PsHhGJrA9}_3q+>-+6Cc*qpd+SzB8If6uRDoYX2F_o>sDPzZagoA)aZ<#`B+p^1uJFuK61k*4wy# zhDB}nV%6N|+3(UCpD{Y@J^1+lm1loeU)Rri9(67M+4>hZ~5_SE;}3nfdoC zRsTNTjF5Nl`F1>G*+I1r^Sn;qee##tDs%RO-`!Gm;s?s-+&KEOTq{F;+S=xB_I>Nc zPbp1)@AkHQ&4m`MBTNV6{o;)|HZQ#)*Q2fUs=$#e_U-l^oQ=H?Crx0>={2(9zT3%r zHpo}>Yn)+8s!$V;>&FexJpG&|^xy7Gv@40p5kGY2mQ~V>K#TpiOV<3BcS*Rv=yCof zy?YCjxjt+CyT`w-zPHfYReF7~?B&{`ib=+@uZx#$oq0+9U#@Jq*qV30+O7D+rq1k3 z|GfX4Vf^;zR{IvLvD159VfKG@-`VsVv&;6qU}RAL=_hUS z7re3f_s6O`-|F6MWA^2lS6?fet+tJAlcdj_n!CY!UVgNfJ`ny>@oUAJ$3a*4jn(~h zEfx!Wa4=W-Yf*cD!JXZo3Md|f2RiVvcHo3?Ul{dTT? zDWW!SbMC1P*^-s)>tAkG%hWj0((*6h1E1PWHLE7G(E63K-~McV&-!gagGi$HZ|VAT zs~)?|{&YAkxpCiRq0*RHEV=v2KiAGZ-q`oxv#n|3yjMSZpY7eje%RMa>A~vvk>`r6 zS3Hioc0Db8`^oS(``gZajBu}7=6h-1!KGE}l4rc{?Au{-_4ZGZC9hqXxc-Nl?EStcwp;$_PS@J&zEyw9^OAq`{mGav$f6I>9yPKK+&Jl09ze28eEzV`jTl3plIrRN~ z_#k%HjFTDlET1Hu`_A2QKHqHnQLprE;>No*d%u?CWbasQbF+Ej+s4(id)O!2Z>`pU zmmr(%e$V{N#C3L`9D>(brNcJ4^jo*!M(yt!~B2f~#%seLo3JI=^Xk^0i0v_VR@BR|Fq6Kl>nJR_Viy8|&|E zD_yei!Yr}ZPTyDF-8J{!B>(fX=aKE+N*Cg+?cd9dA>XxWz zdByPl_AmDsLMQiKfA{+RvLAldzxIhXzCZTh^YdEM7k`cYio)G1?(;O`7|h?y?Ga;t zHIePDzz2z<;u&K7mwx&lI=u0}=MIim_eE=!uB|y=$a{YNk@T;Je$~5`r)`dKZqQtl zx!C+=#Gkphj=sDD<+}Hm7naYWR zjjIziD%UFefzF@TVEJlbV;?JJ{=y^I&uM~x=^OjMmQ&`JKCyevZ2o_T^YMSlkCm;? z9s2y*&dn-ma{IzJEPGGzUc2(UWY45( zeP``X1s2WDKfQX_6+fQWE!k3~OG1(*_{?{jC5e~b4L$zPGN|I8MrzE}w4Cj$AFi9* z*;jJE!B$~@vOY`Aq<#HwEazV^{I-9aF$d%PCGWHECGY#|(Bq-PQR# zg72-dRg)R#6+@AS#d~CKKD#NR-Q0J5siMratA*2V@h@$9{Nd)0eYdIu+%4Aq%;@p; znO0a~8g;%~PI@=TonIG{b2Gdwuj=-&&hItYG56`>pX=t7E)jV0>uyi{@dvWY|KHlP z&wS~>-|r{x_*a*#e$?Z>ZF0P6yZXsnKCdSAiCFntIo&)kulv6A`mbiwxMSRRS*h{v zzRq_2ZC3O5x8Gi$Z!hJ!ihU>!sTKN1(f`hg=XbxXoAlhQ*2YKv)3H5)Vo#57i|=>a z#C}z7%DVFTew+fNhNQ@7h4pK^;^-oE)SlbhAP^xZj^*K++k zvs5c%&-OcIr98>an>yFd(^b%~P$IHIdE=jJHTcdn;``6$9c7Iy+E~Q>0 z3wt5(fWMX9$4PTy;G9%huckyjZ;P%V8$@_fRwK?ad1Rr;l+9$(!)B5-t#nr)A&wRQ5cqz|0dFKNs zPqis$WbIgK^|I-W;FY^8`zC+s6n=NXkhSvCo~M4N>^>a$$IT#jz3$KPH1@>n5oQ~> zLl(6CKg#?dB>!vS{ip3Q+wBU1w^z#Se|*Hhps#u1T)y91-epQXnajLm{o{wd&c|Dm zt{AShkL5n{gp+;Ymm5B||0fp9p0pO)Z$8Dj`g8v~UWWU;>fbo72)<{~@SZ#6c&YQh z@>2h`%!C_IOa3o7|MeGRjoi%}p>oq|+9sT1sBNF2#C1$;H#@)7(p{@w?TuUgzI=8W z&#j{&@n_Axt-M=RyP$81QqFTmH~s9|+;v+M`ph#eUNN&3!k#Wp^?dE*;<4cj52%nSF2OzK;)& zJihAa^$gi){i}_R^3NnLs~`KgpwIhgK*)OOA2$?alfU0zl3W=f;k~2Vexb6?%IJXO z*8(<`&OGK8c57l^src!oj_Y%Gmp$0=O*3!H0h7d7+3NnL`Kt^fGwWwO^St!uxkNLo@t#1bJu~~h>|OenEN^Yj(YS z9JhOm$n>ho7cMU9Eq|LZ!@h-~^suYtR+o(Q>BdXnr2Ovx{`aUI-*0>t6qv)X?zd93z)#=qjsXCdwu z$M5d#ve1(aCs$vv42t;vaj8{j!=7`ztu^^zxklwwh=EZ+fxK)s3qYZS+EAUvebAd+YXSch;LleEjPB zcALA)#9x|tOY;^-=;FK?oAM_J@A|!N*Ie^m^0WDmf9YesFI{Q#p|9KO+w-#oM?z+s zG)&)ra`*Y4laro$d*0t`wQ~2DvaAc-@|wJCRVUeAE)KGesC>C0{QcKmfyc8?);qap zE#J=WV%7Dm{(}6?9!{T`=9f0={c1Y6(d>!NlU?Cy-;d4gv)Fa`x8e8e*2iyt^|SjO zAe<9-*Y5H@wUzd>{%@`otEg}AG(Kym{_;1|bE`P%mE9U|ulvupEqfc1o0PRYWc#y- z;|Ikwj$f{~SiU~|zL(`KxhLY+4+m`z&&}Tad{g}0h3_rDEZ@}kp|4X|!1jOXd}G;a zX_hzN6e3>neLKD~_PY6{Yl44%&ffLh>YCmAlTW`3$|mLreyQ9Z&X72)!aBiBva)1JTF{<_MqEV~8Dzh0T|`)6HpWG}OVd*&}KFoG{ z_v)inT`oKSn$sJO|5(#is&93v)@9YV;E`PCF@`U|mL3W49Jl(gR$hJS*_)c-o<9w$Nugf%C@Aw?w z7n@+as`}2mW0sc-r)7PN3i^KV{(rx>274aOeSM?$`Modov-?D2K8CngFi-_EdTuKcjB*q zGsLr8cdA{GZOIhyJZ^KVDCdFy-fsv0ZK*DiS$e*u_>9#x9ji&@<&)W8i^&>ZRsHU- ze$)42`@OBbFQ;v7`m6asX8qeYYm&UJC)b2c$=WFQVs`bPD7Iu}S$4LY{x3StoxJ<< z{({FUuJQ-1oKiIoSO5C@tF`ZIqA>R_S*u5n_v?B2?D@m8{F~3z=+wpE*B?Au^+QUW z%{)W6^iB2lo%{kt3kvVc^VBYRYW8c6eki~GwVIm_H$9HO{=EK8-RxJ>e%a?PpZzO$ z|8uAC>$~RG_?qXxD)_Louh}d(wCcE`?CbYxGj3U4xK?Pe?SgZT=9GI2l-JfL#@$-e+MV-~=N$ZcUckz6VzWS#hvI_^XSy%TjFD&9KV!dvq8s6mffiAd4+iBajR?2+~)2U8_Iq6zciX+ z`q)Num0WPjJEjK?$Md>2_-0vO`B>GJaQEcP#p~W)cdou1kg3e zQjmV|mEP(puZ)&XYs&tdY$et@n|t5-$1R(G&o8ynkq$X#>1XA1^U{k2{U;ob>zTX$ z%x^RMsG`$-=RMok_5WV(EM1lSYB3*w{%x-0Se8S{yM1$_WM4*X`y}V_j zu3brS%y`vtZ*C`($g`SNDzVbvdwSOy?ekBH{nZz`ea`7um5Z-beSaBYd|5D^|5i@h zbN(;(*9`Y2U+vDS%guXbVd-v>N;p|+@G?DrQ86`D`8h|`p)7t8+39x$H*hk}v0rv# z!A8gbGj3^p^S|X_*I(21fYC{oo$qT}*QU4g_}i@>$pm)^UD;Un#5c%fT{8EDn4`Pa zSM|M=*{?3Q;i-zJdxl!%gag;-@3NSEeErjFE7+&6f3ZvNu2!UU#P(BRMb0W$&G{F+ zX}WBFXk)uoP}h;SU6*PqHuUX(A#YK_@c5rbM!$>x*T|da7_0vL(6=|R_Wlw0|3EC? zgDZLA$JerMFQ0VdOoFfE-eiWmCOvPm-~Mh@k9+6&%kJLk3`vJKOER9cPG(+Tm_P61 z1=hKbA3S@rEa$iHD)TJ%A^ zO=}bUzAnS=N}RCl@887vNTD$WY-(S-B_@uHnD?_2_{0oAICmXJNFDH93Odz=BfT!H+4$YqyEPKx$ zkaK=MqhyImgMh5}-$E7XyO+P-C|WgrS)VlbnN`U*Ri^uD-Cvq~(Jkk>#oODnlXvT` z4V-K9|E(&!Q^V(3T)llIZ?-xY->LO^f6MafM}t+S-|w#xzZvDnCts*oR#B94_rJ&S zUG-&oK^!G-nC|(`TE6G?{8w@Rn-kx?+*tU^)AGv1Ygg7sOuhEZSXMn#VsqbFgU`Mi zwpU}@%T_7RQSo!AF=G^YW_#<@Qg}%LDSZeX8BRetY7g$E90L4wu}2b6LUMbe_X~ z>GziePgmVvd3odF^yKTiHhxPdIIed7Ncx@~i!H7@F8G#Ld$dh#zsnn@KL=g+MwCx) z>rc>+eEWIFvc%Jw21)8qQg?+h>vO)96}Y|dpRlj&!SsFJhuxhc?^a&mo0DQ^ZPY0D zYKM0zN9e^Dx2_8It}DFLZpHU*vFERK(Iq|Er%bm#Ire$)4%Wnf3(NInzVurqNzF>h z(Y}3VxyS4GztxgMU;WR%Y%gY3+H!cC+K+vGH+=SAJSfK#D|?b@_vd$yHSaA|7FpPL z<@n18wz#TmAB%D?$^6&i4XwQK`gKsp@)_@D-%6jiz$Cub%F=ntvf@ znRfl%1!QOr!dDLwg-Z?MolEvzXt%?KazO%gcH|WE3XW{M4wtS^aX58N5c8YPWzpqbm)kV{(C406cmPOdx zR{F~RcIL8~a$@1@XRO!X*6!x$zO1l%$J*osUw-b~_Sb5Lo-!(dFRkpZ8oo_B9~$&$ zF-On$*&8B@ci-Pv{^ExC&zR%?%Koxk`1jJ|6Fj zd5%*j-`4tFe;ZbJ;JI`0Z^ickpZ9WY)Svc?Iaeql=bTmCf1cwzRw`E>*!I3*hvV@l zu}#;dQe>x}Y_%v|@U%iDKG5)@d9AVI-G2AO{P(l!WaG3ada~>9s%?AJ*q80|VcoKwSHHjIczwf$LBwS4 zu2gIaT#B$3x0ZN?La&3dd-_4q>|$5dX>D@$VJ$W50_2yg6uf`}lvC zeVabf$*^Y>eAY(mff+PiJqiB0-*&9>j1r7riR zZbn)9o;xSgJnXKoHQUO*jxw60Chc5V-ZZ$8ncwO=xo^8@L<{SIIZhXmaRLRhgo9WxDtn=P4 z;`SM=Cr$1P=CW9pU9RUd)&Yd!!7JOG_wQtFuFN-gy zO=kSgTzG=XarxQR)Bay!Y`gVf64U3py=I*)$F08o+8fs#;&tI8`{{zKxBu2H3Ayy$ z@cuE~((eQIA6TjyjrN8Gcb zEqi3ErfaHIJ_uSldD%=6pAFXhKXzSR$Zi{HezG}azp%phH%Dhy9I#)ce6zXi`2WjR zd^QKxBs1j9TWw$U;I-Tr*W3dO=9cF8uj6rfFM2}kMrU8C{f(pB%qH(MmVI4ka%^K? zE%zO_;}=+Rm+HMo7R8n5?xsG_%}uvDH)-=Zy%T{!%4rIG=ahX^*Dd_Ca5nGnSk2r_#;9hQZ$j(pBp6#{#P2tsb`)hw6{}^*z z${5>`(a1a7@4u1ba#*SV>D^@3AL@KJbhnyI)PFrURqbT%cCt+jrqYKrdf)#mU zQ$DNNYo$8qDGUj080_eHsKw*8-3+1u~)Gd$uc(NX$$dHXlN&3zy1Jmx*9V)FjE z&%WXSyV`>dk?&O*P8QFyn-sstj?wS$$~DRH(t3e+x+gx8nU?W*y1@a?|Fd35w6C}Q z5a0ayeT;imIfKk)iS(D!(fqalPM2=pz4CAD@i(v6>A#xUx5FaF=}ze%^}<)h#c!qV zHyTu&$arm0Wx-RMf5Ff$*!kzNGb{T_{=EKp7Dv&!%gLHuf7zvLFFB{IoAYAYQVSjR zV^2RP9rY{%T4A&aloR=+gdSrhm3p|O?IE3xTicb3F1?)$pDpDSMpwiaN$s;yez z{=SQAPfgr?^=0GP37Jk6do*ojz8TJ6_SaBj>dM)vbC#WzT$z18jjQHb!GtvagjwdL zYm6V5TeYoQ{_>%H{{K6Dg{zXo58pR@_4UD)8kujuldEs`Upidqo69G0OhMLNtV7~C zo9yRV_ry2N{&s(#Sove#s?B|Og(tiVk(QmF9Cv)-mR~1gt=36y_?y0J|B~0=f7k8( z`TB3!TSG^8ues;9f7xBqQX7*aI$Pw!yNc!4X7b+MRI=vrLfZ#l#br-V@YeeIF5*jR zi$sC{U+?&N)#dWPH9zd$`=;;wHntg@YxcjLv_*g4vKPP4ooKo|CpyOnYwrGlxJbJp zl|fzZ$y4RZ3m50hr8S6pPpW)-Kkv!rj@gZzlmAQ%IoI#~_v-QLOXVT2cuyxd+_!%j z@x}Mwm3@6Bc{K-jHm-NzQ2tWdQ@C%|-$NC`vW6F)p1GO`S*m!7T z-(>FZm$IuFW3J7gD8J>~@%E1~9sjrV*>jCg{LJe&hDG=%qjEf>|0KAn7K=~I_`Cj zzgGBnkJ`>JmlxOBEmW3yJjd$JefKRp8_p&j(zSY*BX@nmajR*;$!5?0@9aCPXrB3X z`O`bsW=u8}JtooP|CCu>^Fgib;dHCNFMk~0Xa1rhDoXHpcj3w!(IcLZ5C5&-ByWH8 z{>;0q1-;Db*_Sp}PAI?r|KY#+$L73@Sl&?np0_fWBU$8e>aUfz?O*pYAKuWn_ql}D z*Qx&7|0%g&{}Hs$++^)^UHSTZ{hLKTT;B5Rgb@4n`v)tHFHQWmzr66Be7)jZsb6c~ ziR}7kuK8fORZ>}v{#qL&EoVOM>pB1ZOW!OzciiHG%-myd-m-7~`!?#$s~h!9*sB(# z#P?zDf7Z5@d2gjRDBOA4*s-8oGU>_9FWcr;K0kLlXif73PTr(H4$`^SmjoZ_{Zo;v zFI@d4w{7+#ty1aNCvKM=*I=K~!gKqRC10{j^xG8q*;dP9UFxNBZ{K&9dc^L(D({NK zmitq;Nyi*z_K$gSW9z%zS^G}-+~(fNTDr!{ugT5NKkSNO&7xf$hB@0`C*<->&g8$L zue-&s;ojWIHA{1r@ypMyy3m{a_+Y%bs_bh+xAPXvwtm~K^-aw$2qwqPW;1Vami;VL zUzz9qc>XJn>^JiF4!R`It2*#g)}#ET$7?meJl4`0$LRCB|EdK^e*Y31Wpbx1H{-QM z&+oE^@`-Hx*`hm^wOwR8pM1CW$#&qyC?mTwXT2o7&aB@{xi@Kim`Ue%*%|<={8>dUGTN~VY_*+W+eT)a(iK) z2zyD&?DP)@YMqWB{P@48@8OJpU)If*Fqi*d^yA;h`&VLfiKvc9`LL=EKa=HR>1cx<=&H*_W+dqT{gh?m9k`YoGJxxz|^oKf9~Dw!YwY zu*0qeBIK~{A@;{?K<~}_r1%X zOWd!2e{tjfyF2^7)OGwTEuP=^wQRE0i`gEj(reG!zm<0X<8JZqO9%7s6WOoo*F2W* z`)_NtiQxxd$G`5lOVj=zQ`AyP2Cb=aom{!+*0+ zf0}!@;R4h3t$hU-7AVUkdo{{0VgLNjUreEDnX0Vk>bZTc*U!G&-1j%+ol#m;aJKA8 z*Qv^=bZ;X7N5@*c)o}4X2Y_+$*1~^Wmm6#bSEi!eyF=d^R{ls zmy-^0`mkj-T^D(8|2l%tuzI~y`0DCd?>?|X8- zPX6)sw`#J5ch`h(ovFTmxw4%!kC;tO*z7xV_PzT2P1tJQG>heh>24>puc&i#Fw`9!A7tmwU0m$*EB`OzF}_Vq|* zxUM4ZmD+WC?$)zL4vMy^p4Adl5)Xu|viOrI{p@&~!Rafn9{Za$M<_C_6*j5Mdj0B# z{l68D*QLY=s!tVKct)1TtG86aC)W4&+?O-fNZ&mZn^FCJCwFAIpT4_RY5Dv<)8;o3 zbIykzzZ>%I!pD%q7IyPjedQ_0ytiaQ^5ccuSvMyap4j^J^(-d7$G)e&2YS8Iy`D7Z zz_%m0uV&|!(iu?H#FF*TBT>Eu} z|Es+C;|5j6tQFIl{4_5JZ+5WUezI5DVdb6EZyp?*d3w#kJvlVNDEz-lrxmz<}bqMN_hTjS?aj#qu+bM85RPLlcmE8QZ!`R=Kw!fRc_v_xbKV&&J76-e}xP9&6W8l0$CCvue?8%?$AcWed*F5mWgx({Eez$vLki4kx`_f6wx-^)p_pe+&6uy(;cc zZm_Scwcv0(&7aSvINvab-8bH%PUhcxt4;QE<{P}fF!3IviRR%G*STzECNUVk_-nPP zzk2(>xOw*KR+n}Q^PS!>_f^OGb>6^AW|nuNcQBf4y^SsoBF_y|b|whc&^L%~PH<{hSs5{{OPN z{f^iBo3C`O;tXat*L*&sY2%WFb_?%m+EX-cF0uJdxckQuyeB!qnZnP2O#inV6>aTjBK7OQ!Btal4<+4wfuF8^d$`zj}#` z?Eix&Z}~dfZJD|!?)!@ws|8};`_5_d$($CFwIzB{VK`e~@BzgSMUOdrPw!#ZJaIhp z-rp;4_q;9Lz02_Zqw}}-Kd#wPQ*871{JXv{e~xqeboBH4t z=h;0yb35MG-2LO%VimCeZIw;g`-BK5Kc5d7cVEd~-tjg;d}8qSODR3!pKsl`wK(AT z_rrVR4$fx@-hH~MeVY2L?`aiHe~U`b%#u+Gdsh8;scvQY`ufYt?&f-58m>=Zx7)j$YhK~4>{qjQgx(LX|M%@r_Unpgs`sZR z+SHw3*-;a{w|`CYmj~b9^;*CGCTrz)>cGanm!9k9F&x?Rnsd$S%RYNE`Gcv zmge?4{@3yQrDq=Nn)fX|iZvu3SpQA;{M=L|wL9dWYMQ9ZwzX$B@vsCo{3^U~`Ab2UCr{AM zpL=wj|E+yr`X}v`%+huLwwd!^SlPGRo6-0KpRY#1K7-P|eGHF$g4)$L?{hxBtu3GF z?}GS;Rtif^+jrK6%HQ*8aFAX9I^HLH?eS#U*Zb~fe>8s6*_qoBQ6o-}wCh%-z>tbD!I}%}luZi0{&W*Cf7581w%2KNp)@ruoAq+1SLK$dOc2v{{^ED_Sik&> z>E5=rj`0lN|KIBEz1ux~#um@V&il8mub67n{_Er`Pvyji zH}2I39)IU_$$sTaso7JCqu^l@M5k(Eq;S$}LvQF>W1o&8_P@lLbJJO4fCZhQ6h z{r+nozueo=U%V>0wy4I*eNSHbdxwS9rtggE3U#&<>_mU)HS6zWv|URJi;dDrs{ z>{@*0yykEWK68*I@502Kv%9U9Z0U}b-EI1?{IUFNkKY{c{caTfscX1(ea`p&-yF(H zmtmSe8+>7{LU4D3R^VO?@=RY>GOP0!1gwKBb^q#P6_4$Y&=UNTy_MUDpy>s`;!q>c3 z&uUk#za1>A{`H9Z-yf2zS0~rrEsCCZ_SDV8T6?c6^xpVVIl1poT;o4$i5>C3_9ViiPTThR_WRkr-3}WHlA`{CPF->D z+;ZG%-qLmFOZGI!id+(WBUmtDzeBx*>|>V?I~gsm8y;IY0h7wQ{(l?M;_-B9Qc<0`{<-x<<|qOe6}y_ zKeRQ(7(RTRyZ?sy*1(fpj`!dFX53`|;Z?Tx+`~1m_L=+@ED_a1)Ic(`1ECud)*f0)((^|`{`w>M5*RmXnt zNYgoQ#){``@A*3JJ>1v#^^t+5ysOWtmq!Gz_di+sdj6Uh#m!Qc#f?_eG`^d4fv@(lWn9konbG0L|5{I&Wu^A2_DK5GWLb5?m$U4y2+A?2 z{j0gQaex1xvrm1F>nNXnervt`_F~!J#k2P?Gwpx;Q1f5twz;ow^fxD8*!}rZjh^7; zOC@E0GVdB;ZKwY@_j1=1FUOTU3H%lEN_o>}Z{+cmZGXeLbIYaQnfqqno^{%(R&y=S zv2N#pvNtNF(#PhcM3!$?i9Tq?ELtjaL^H|n*m8@rS&l2;>oHHxc-*m$!Dy-eV+XmmU-KV`g-k-B5nfmNyOs(5uqm=j0KYg>0$XfHK!TRR4XQJ2U zG_p(CX55{z{`~71*F%hsPPVsq-S_9__k`p7O8)G&Qj5P^zb1Lx*0{dtqSgPmcgsv$ z-&4qc+WgYT`5DEllA{m%o>_8sQ{Tis^SccT^j}9DKl6RdzP=ZZ^ImwEl+KxwZ~guC zf7j!2`-^2?%3374RFxllptyYX<0URi^WBzP{d;=z$9t38?c7qr6&o+znUyZA3kgn7TeD1PVNkd0_VUB! zrzZr?@x1MGd{_MTS!cHKsC-#!X2qyx^+>gj(`JQ9Q|=U|%2moYd5WsU&M#jSU8eW# z?mYA3R^Ki)KmND5Z+FSZY?&zY*IRZ4O0HPeH~Es^8s#^~6Se;Di#I>J``W}yUWQNq zmrn0%&St)4zc5+Px_sKH`kcl?Q|?!nEcx;v_SJ`5b~lc1H?Fi-JGRsD_!?eY*}1Pc zo*&EbeY@Pdy!6cR5A$l@EPD_<>-)Pq5B~nx-6zb=GX0Fr<-%yT&Dp6;|HL*|{y8bv zVBWs(Y0JE0>S7(9l4q<-Z1(0oI)1YJ@#Xiu*uxBIC*Ho3b0^%|ci{A89q$Qyxl`o$ zA5_!en?Kb|3M+DW@-1_eFUht26TV&oHBBa>UBQ+~pz!vEyuZV?CfD*Fou znop&!c=dNig~5C6!#j95Y|F!+uk16KyZI~seSQ;-ttXyeaXDUf;=hZ#&69%sto_%U zEo1*zh_gRGw$Q#dfVEtpyx_ryyOO1M^eg&i?P1Qm`|eG}ebMq~{{6S(wjVxuL{~Gb z`p?Y1>4I#}?d6KC{?ERhyZ?X5eO=S4vrc=L&B8k9RWtwDri4yAuY-4v{;&M~+`jPj zM@!qK^KPq5t(+TU&)yo;&UwfEm5rAy*ZlR5kM27fQ*_~9nX&Baoo7pQ)V432EoURC zbkvcb<;c!uH`})5W$rb}@HyD_b_W;F5>wUL=YOw!^Qh$M4{xrR=;NMjF+I&zaltE- zrFkQkCv5Kfn_T*K)?Du9D;J}KB-zDyHa+Mmc@t-T@tVM*Ev0L=|Mr$#k$qoZ$Jyoj zqsm#Yg~ShkxxD7*u~vV{#JuYUSHzy*>XzJYd)08d$2#@zoU-nz>+KdjezI$4{PEph zEfZOjiu#|v*0A9}{L}sR#gFmtGGG3fzqG-Bp0t1POZ$b7-%Tru`jx*`@5qeP%kT1> zGOIDmw14vEn*FBJCHrT8yM8uV*4=Em_w9tg*4G4eBsQMJ+6zG%3p?5?OTHbFfmDC0L0tuS6BDJyWH)$SKuXcXdmR%`} zSd=|C@=L=v+aJ4)H(1U!sC&U>?vNL~sLe3z{fFG&9BZy!Eao?umv~mFw|>#eWWD>} zu4ettU2dpmRc5(c>GRSXyYtH4C;aLG&&j=%3jrMA*HZ=DTalupaP=QcNuO)HpijfAnUm}vU72fAc)<73meQReIZxEX=PI z7Ug~`eR6c?H}}c*^RsKO2U}k)EU$Tgq3}~&>q)z9rAOL({t4e$+4scpT>FPQS^MqF zzLhMgy~AJsi;r)Ixqi0R`u(-C=imMKo!xcp{VR{;H{a$zE6Z(un3(JLK3RHBa;$86 z_N$xCHm^D!`(L~A>$m#Lj^96mvp37`Uz=R-Yl^*v^6Zk9A?w#!^EZ97^^H9zDe$$$ zg7w?$-2EO>^MV=_{gQ%wmq@k;Jt)|5dCjTobFH>5yQX(v+)*}|>jT4=ppxX4kBVz8 z-aiga@i^EuJMgEB$6YVYfa6w9URE`yR3E=`{FVB{uh&+lE$HTkD=%G-&t-ikCfk4d z-P@8i-#4!2U3>fg<;B;{E^e6~_Ure~?XMQ}MJ($0{y*k;TvgVk4+dxFmd@GY`{JdA z=G)fihZpuOe|fTy=Zc_r%F_Q1_wRm<$h-OGxS{<*<>dmt2mY{1v}_?xDbU*9F%K7izCz-ch&5`S{(c z>-*(0|N0v*xaoL%N&5RA$Nv(Za5^h?XKmuo`)^OoXS~-bU%;{XY4e{I%n9P`VY?pO zZ7)~Pd2{mg$M${8`OeIJx@3Lw-IB@7za!jr0=IH|)xCY{oEvb;Zh5LvVqPInhx6ld z@4s(O1?W2G+!Mc`68F&~++C{W-dq9x+qqWP?$y?mGwV21>{;m&Aj4==Anf}4eFDeh z_`jL?i>Aro#t8WwPuW8wxQ2K?!o*j z`Q1x>FW0}jR2a~?|t{2*F!|syO6#jy}%ijL_ zw)e=Eyh+b(;`*0c`5jwt+Bav~!`XA{Uu#GFx*oCJMW*hdN8;;h)2Xx0wikTj>x;YZ zDQP$BsTYXLs@$bU}_$iE;M6!1OuyI<)WIdiYr zeeUjlj&}C{Z)u)r{?xf#^TOmSf4^S2Gxu`(bNjiEU&?LYGI!ae`OES?e|#5Z@q?_wHjf zoiAM$w4rZ9Y4{TX!t3nJO*b)j?){T`d*)iDudeUU`OXvnmVWKKexhRf5K5l z*~QNVYg03=9vsYlHKX?E``>%t{;u0>o||s<=_u1t=x4KlQ!moGt=r6nf zGy9(0wf%A_=!|pyyKVZfD&|Jt;JJU-;G*E4iFao_sq`=WSu6RvEc?6x*Osfb_KP1M z3$JSKEAe~3e)jsX`xk#(9$=QUyuR_(@#k06OO}*xe{=s}VRPS0S(YdK>mNTd=3Be{ z#rgX(@1H)7FIn@T?)7SybMutdzAy8CG2{Hkd)1~zw-@$lrsU^V{NKsFC!N90_Y}jW zTP@e$ewAfWTn3+_;zz2M9X2uiahS!rbh-Y_-I{5VMjLw?OZ5*)*ry6dvHSgAwu<}g zou84CToR8ASZk&gT=Q={61Za9L-i7wBTGb197)tF7fiC2OIhB3;8Naf-Ru2lYkh8A zVVd*JRhIotn@CLMXrAJb$NB)XT!eCvkTL!*QKvK^jj$R?$%B7t!idnJ)?PIW%*4* zHQV5TH@B}8mUYjSnZutU{Nk0xuHWXlqLmX?@SdryG+R{r*+c>`wgG|Eaf`>zj56Q#{64;Wz*iT zPm90fD)smdlZ}Q`@b8#KW|{V4vIq4$%M=QV-uBf;yMKsmJa2t|?Y9MmmMe`F?Z5K1 zeci?TyTn%EWRt7h2V>dI_FXkk%U}M;+w!+ec>YhTdkf8Ovo3fZ{`|4T-;L9gXSFHs z|1q<#r1$NggR9T4wLeyCb!~pc@q4~{xuvuF?#?Luvyc7%E9>V*A&ERQyuZav-o7{I z){mIq&+p4xom)5C;`(>P`^Wcwv%Q=a;rzFcxkU)P8JHOmd%Z|cHD9W<`zVGLAiB-81i)DAO?ArJ~k9EP032AqI zKOg;6DlDtMvbogf+Q#{Er(XU{S^cA>@9)R{%Uju>n=IG6zeG98LBrPM4139vyUV{^ zF=aLBmr&aJsOMX~Cx7qb=DrO67t1&Kd1=pEHtDl(`!(HPHLH^U&-(X&ai4ND`?Boi zC*vMX@!fpo>Fts|+pfGW%bET1hOguMOo{EczrQ`^Y2N33{DR1_KL@kdtX8fqd{h5$ z#!HXa{`>FST3stXJ^l7yE4OI&wJQnt@YKy_%YR^R>yv1YsL{J1w$o2Nbp7j_S^r(W z8@y|C=H~brGh0}~vv`Ga<#%$Nbfbl?w5x zx1|<-o0`9x$Mm$oM9zt(cVzynK3UlJC8^VXilUP2W5Iay*L$rM4q4qRFy(>*rtmC^g^q{u|DB$N#eY zRXnlVer9^TZ1nF|jfhv>Zc7Z+-n;Mk_PGAb=?A{I)_pXWoz_0bS@!ZIuJ>OXdeg10 z{VRABbNtr)+I0`t{jK`nTKD51Iy+RK@V=KweguUg2!B{hi&)=eEA(??0YqlyUue z`Hsmkw`-RxS2P@WZoF~9;`97#_jEh%y_NGiYw4`3tNSLOj9uQg?ENY<@u@bSj-zqh`t%iiaIVez4~-s$-I#`P2X{<|FCbpQR6 zd*?pim3V#URqdI5pWgK*Z)Vu%ar{?HMETBp$7}aK`X$SfE6ZMb;Nv!CJ|D%+v#`>Kd``&lAPZvFUC1{v( zY2qcH>T1iCUz%eV8m23JW4k+*fAOTyw;$akCm6DRz9bstaCw4vmC(|6-;d6m(s6L# z1+&Y7K|2j>=d1|+kRB)IWxTiT!OmLK%U1)AAFMRKxUo9c|CB;+iAwIS%ze$XgZrE_ zw;ip}zb0=yLH&BfW}d5Yhws@<>bv>O(9`klOAE`s=h?4Z+-ErKcDa#lDN`QP{o9be ztan@A^Y12p;_r^HO!}8vgV`};QTUH$mx zkKH04w(76lpK`b3``TaC%x491)!MC|trz+F*jEBE5t zUyrI6y>Ng1%-6~(cz4a$_X?@uI$Jr8?_Yg(iT-N|sp*IB+usX0_wj5`RBn3sqyJ~v z;|^`)3AaC*r}Lx!!pC3lTDG=c+u#2}BKKeFgz_&UkBoZ$mYdJOK4_Dy^)Zv{hst_6 zk3KhgNYv*oY8FMBU*6?FT_kMwhw?ij8${APQl zFn9Ku+`oH{^}c+0z0QztPO|I%@>=`#k7qtr`Tq93@?7&h{|!tfjOG8oF61`ue!TzR zy2nd$r={K9ZDzFh%2ShD3;SY}ii`i=FOYru`&s>h$G0}i@^aYWYvm>uL z^ff!*Xv?d+!~W>~ zO|*GnV)s*&{r8O;_Jik`E8iS!|9CRo?BjyAn-0=PZ@#Q=@2kJz{$bMo2Nrb`PrTiE zK?TX)^R{QPb^qbA>G_vE);-CrN`<;ff`zq_yRZczT_ z$KQU|+RWwGz5V^|Ec*Yx|3dwF*MgvWm*f9#S!F!wjVLk9nnn+w+Jl)dY9Ox>|9HDI>YvpavYqvxFWwMwe0`2CC{*IJhUZD0z& zitJ~Fr)A5(A6xVI>1U7Q&q_B}{)_p>d@bJV^|R_TKYx{X_f_QIcp2gL^vciFTYLW9 zUHEv(mZD1&ACFb(rvB=eMhQ=CQX9G~c*3(dPc_oqw+MzL8ye)Nv_KsXx|L4c-jIVvXe{2b_O#PS0`^%Eg7FwP)pZIFVmOQ?7$JK=S4JvOI zrnv2Pic<3HGX|{iOTHyD+coQmo}z_&e&&hujKB7x9`<>KT3R`A6@rirdn+B z)%JaT8+%JkunuH-h&AM?-IdpxyT-vSH)pzC^XU5{s6-yqg zo|Sq3arVcRbL3-WYq_tz>yCc^WBJU5rAtjW+sqG^UFec&xA^^~jX7M-YaU}?oP|`L z{cxNA^PJxGJm=(mX)fk#C!fm4&%RXk!g%@H1I(qnjz^r+>bw7S=W>a^hCBG8``5D1 zw9@~3pjq-)(9eA{`|j4N)eC$-db6~2iBN3BzSno3e_v2;TE6b_k&C^tN?+sRh%di!Y3s~vKW_uD-F^7qqR3CnW_n0u|(<^D}Aeb6}X$j){DkMplu zo*vj?7;)L++^?B6$FeU+-rjxQV0Tqsy+v2~iwgIh`^pOgjz1IXpZEK^oA#XBzxQun zpl<)}O8DI^HMuPB?e=|E+;(T_t1bpGJQpFTKiJI!aqk+|J?n}G|kK?U+0=+#dD`SWeJTh_wP;={=&XR zNp^jG^LmZycc<5R{{JS;v1mQ}AJ2)^B{F-$8=n=gQjRr$*rdE);&sIDp5QYFttBe{ zyq;e>-_r2%#B&Gc^@S+gWdXzloH8DDkj zm-!{oHq7$pJKvq#y1(DvzGQ>l(qw(LYCCnSYjPL9?y>CN`TF$nx1W4AN7nzcdep0& z6H({!_;8#4m5smO@m1Zc%3Zhd^=8GYmoImnHC}a0L0|u*#cr|Rbq}jco_u_Np)hFi z+?sDMe+rlGDeIZM_WS(Op5E)v^{x6o{$)vUvae-lkNwxIuU9EPJ6rXIMK|Z?gTbOD zGIuTq9mMgfwb}?V8dn{G<^_91gE4OpZS#$c1 z+RD}GninRE{3+f2VP??uO9Iy?Kb`mGi2Q{wF~=RbVs7TIO17ak~r9wem z8Rl#AjQ{bk*%;FHW&ef3ZKtk34&G%mwZr;q{jy}!>7G?q&-jWYuRIp{vg&^NuS<5% zru_e9H_h$Gs`DRL#n18IxAfln$CW?NTwgtx=}hT?yu_rXFYKjTlbz3(l*j~gecCF2 zG{$~%Y+ula^lUAYiK+~1t(#5Wr)KiKKI?nlEmrpOEV0r(+X6eSYVw-Sy}V{~apFFY zi{jg7mn<>uFNv|cU~Bq1@T>23H!G9lR^Ng@Xr8UVE0DBLdh($#o_+s`$m#4HRh{7% zzME{gx_D}mm+-`IDs}Tev(>O_!}`BvX`8+~Znm!%V6#22ep7GLyw?%c zC(GVjCj48MyiNVubCK5()gL^MuWQS9%b4EST~g}v??W>GzXiD;72eq#u+`74I?ptX zW6km9-%~#r#=bmP{oK+1tHSZsA49kK)!f~v{UiC|T|txgU*@&0>-+F-lEwD(FCyGe zJ&wKBd-l$?{`nU+{%4t>-#f3*dcJ+1e_T*Z#Ou1huhV!3p;z-}H<%&)v*!zx~}f>$$|Mj_VR@&1-(gm$&rs+FE??|G%>DJX3Lot^UZnLvGQ`tP7g!>(w4bCk){ijQ$H=}`@7{@vFv-#yMD?g!4K@0 zC{H_>^V*|Zc)3FT!KwpG=e>}6ooW2C`}!l5QmedZx4Uz#&&KTQWnH;Gd2ej^;rlP| zT-`o<*WG^0ixU@qeQpw4H~mz-SM8c~1tN40dZ?~(Ld)0A`SFB-vy}s44X-@@{i}ZKJAHQr`dTIJE-z|n% zt7qh9v+c(Ro7rEz>SvI@eysW1=lZPq`VkMBw=evu)OK{6HVV7mwd_bolWa) z+jHkze>?lG&$G={tNnOSMBSi@{n-6dpKPru8Bb3aE>O;Nj+eR2STXg&kJHM2yYf!Y zocr3MqMYd^j#6^&B<5*B@e4CwmQ9n}JIn*XTS21=?j|h+DtE=<@b!B3*oym*Uj;Y zTh3P$ow7XC_0;)YEq9LOyz=F>ZGpM@&FznF0?MCyIS@j9*_^$Q=DTI&d!ZA_4P<+1W~ zhGoUuk~RJRZGO#sn%z`ipDdy_v{tdgl&1K(WX(c!N;FX{|*>gIl@BjX;!9;e4%=yweKbqCQl%(*QAAh#`)ycwZGZsF7 zP$2WV;;ngw{W1O@RqsFL`EN^>J;@wv|3oM5QI6AEH7l))?zb;~_D`wp`_s_(vvKvi z?kl&?_DR3|5&B&wCRt?i`p2Dp@%jILEr0(+R^w?-!RDEoQs#TkeEt8W^vLtJ^}lXh zsMgz{y^i-1o@ zRPaeIkCRJ8O#J%0Vqc3q$(t3drR(xlI%(IDrTM3dUPcKhto?1W*6`@fC7ae4>K(72 z9#*|ZdFjm3%Tn(z8ZQ5;chcM7*K1{~O{ELgCtFYDy;SvG^TCvLS1${CyYLo?yIXyF zbYa%*-QNrrTk%CLP|M8!YFBpIwD{GZ6yLYYHt}6jp6;!syXvjQ`f&f_o|4seq7$$WvQTF>6^2v=hxnw=Cvh}OXcc{Q$Ig0%r}qSF|%m)vdxzNmRki?X&pI# zd`JDdkirTcyMJqx&!q1(srufuUASRgtn6;7eQ&F~` z|GpKs_r2V4m3bD{ff}T=`9n?E<&(oC);=Dt_NQ}{ERIMm{Ufrx>E6#q`)RM7bg#_} zl$5D>e#k*ukC|iH6lYog)60)Oe1Cc4d&ft=;+`1S-G5)77T(7nxWWA8jJEy1w91vg zeVbVFW>-f2^5E<}(wn8`TYmoY|MG4*`Qvj|-1%ZVZ`!mjThHSsS99|nf1LaN=5d2V z@=?2O-NPNM{r=yxs9f)Kc4gn>as!`ZInp8O({I1{wf_FW3wMvd{X5@_&HR3`kp7*S zeOvzB-_SqXXvZ35+b1QZJ^m689NAv{?7n`*_s{F!S=)}UlA8SGef5X)r9S)%uCl*5 zed~&q-=Bv+MOJUkdMzxpZ52J_fOoHccA>tW7TzQ5#tiLHk;+IT0ZZuQt9QS8w{^?rgZqH%=bNC z@zI=L%Jiz8^0(KAOZ;9R;LqB-p6Q5M2lMf)MZ8z}>o5N_x)W=4>9^#IAN=1o{(RTA zcH{Gq8Q1^2S$O9AfA8B5Y_)YBKP(mbE%&?mU~}K~ z8!qmj(&zvD?Em|ja@F;(H?A7ak-lD6c|c&frOnG5w&L4rov+UimTf#4e&$`p>wODzW9(b=e(-CRI=-AZ<0mi`HrJmLR(hi9YB)s4|&Z}WfiTRF`VKfZ>!u&Q82WVO6y`P)5n z;%(o5onz;J@cn&h_G_G{Hp$MFE)DR>{?pJ`nN|AAXnJG!^&eZq*9vca>-Bo>YmSw- zC5+F0kP`WI&1&BIm9KvY%NpALc>XKheyx0Ii$(pd`W=qnO4sDeR91Kx?|XUg$Fge? zoXcMS&%d^D=C{53|6RWsnmnOz%aPpqD_2~r40gYwR(tow_f*;Sf1Pfp{LP-Xsv_^n zx$AwiKjvSUm?O4$y>z3 zBS*=K?{0Db(mHp%d97)`XKvulOJ6oEVpn&Sec|zY2aDv7TMs1qOOBK~#5@kp?&69) z`1zZt`^%rV9?dY8U2k}~{?tBUeZyZq>wk0Xdv*8e|3caAa+_D5uU(|<;!>}dYyKh6 zZ13masL@=={EB)6&m2VTG?=*NbP~{dVgMr)=f!eSL|$Q*CB(=kCm0%)a2M zV7k%!dF%h5&AD}ZW8c{Y>(Bn=*R@~#xbW^eYw>yfWxxANzsSP&j`ANt>>WK|zSQ9v z&kZZqvcxG?t}nBu?2>=vP^CBhnz!+;oNdPH8 ziZ}Nc@c4YFT;g$E}+ku%YJtaL@ zN>aEFRdRi+l)O^)C-3HQ^NC6CH}$SHo{-UP6*R}f+$K`dtG-vg@AQI0F_ZKDuP%90 zU65wQWPjcM;moWl^W@tyUd_05t?%`Z3*s^chFLFSXKQj$18TuRA^aG5fk+o4M}<-p2Oi5*)V(l63fVKwoan22tzj zC&KMNs0Yh@vYgCr@$=0I`-tO_w_iQpw))WK3WtSmN7l`9xw%dAzO&qctMfdpt_AMg z^2p_QQAcB&Wk&q*4YHN&E3Z6xR-%(`ta-S6?)kJm`})4#`S++HO?LgijI6~!el#&$ z<2qDkdt&$3b!b9Ui()Q7yPi1q4dq+d$mEP-&(%5C&|o{S)4AT zQ(CdtqC()>L^by3Z~1%d7yRaHyS(kb;kw$#$Lg;aMo)TMG$A;Cm)W$#@xRw>_kP7| zJ|4mp<0s-p3R1UdBK9!YvD}!#iub z7GDW&Y-N5wdxi4S1I7NZC09S;ia?(vPWy@NS2*8{8ue|()~vN&ilXq{wSP&u9T(z(~_Di z9>?pqr|qx$;CDz*Z|Pk#xd;Ea(7yL( zhZXKg@4xruSM;1MqSH8je?R~DZuQsFC!3l-$}`Wmd{uGT@7}V%aEf&+^hc`tI*mhfXd)zGBR~A}puf5;>O-VLU{?YSf z*~euD@6$5Zo1bmmf7|$5>WWvN=a;Veb#&jHqJN(+U;fX2NjaSN@eGsvYlX6l=Rb=& zpSt|<&7b1Q$Cm8ZO6#j$_V_%1|3SvObyQBQr~x7M2Tgn!TTkE@a;m`??) z-QIh0R@8m%4{q~j?YhQkx6Ipeqa)X!S&h4QA3yZt^s*_>qh5O~{abW1_)?+qP16gv z?UT>=RGrwl<-%5@9mo1gd2)Sk-(FH9{W_xDRrYeJHDgK3g43nUOJ&`sb(?IS*Z0Qn z!N*81?knoC$Frlk5ne7u1PMFk52Nz+5$pe z$aMc_`~yZ;{xm_QKh0iS{+2P!Pu=X{_DNl~`orNXD|iH_yx95e?$m`5whe}U=jD6u z-RxwS*mESruEpj=e6)Jt+k>xP-?(g$zy0IuiG2lmXZ{@Y<^OkJzU}7)_ZKGD`W0uM ztp<<%TbaMCcz5vqYYDfEbJo8<``P~Z`+0tX?CYNs`Xcsbpx+IP=tU&V*E;!h6Vmt7fp{KucI zB|OP7*V(GpGad2SJbR7r>6B9%KYvVK|1aeDzpr)clDSxJUp>Dp(p_Tx%H+5p_UrdI z#r}zqufNoMFV<`Z@8z#~hs!?7FR_1F@gr*E6`RVby!)(EEzSP?ZC}6Y|6Z$Y@r}Z_ zlVmLVV&~UKZQu85E@B!L`RGdI@t|k247P_)9GJK3hfn!Gk53Hi%))QcnrqEppSb0A{g`^mpKfK5KbOQ$obJlb;<^%Ff4+3jH|F}fF5&C* z*ROhfK*9RM*5%V39PXDo|2U#J-S}&O`@6_P#%{FFkq9$Hzauzn+nzT+L8W zm7_MT{qmPDQ=V~t6M3c+`@<}kP4mI>&a)wl-uk}ZUc9hw_rBQZ7 z>&>@YsJtxi@)ffLt|G(fHuCcrzr}uDeA=+`=Y5&RVAh%*>Y>(c;Nw^`julOX(C!?Uwo?G%eD+IR5aeSCm_V>_9=8Ln-Uvb+x z&oQfbe)%8YDfbEIdrwb~xqZXB-AeDh!h**hQqz1j^`eycryYsN2`<3gd$ zAH%s-q>iwwK7M7m$>{6X|D3YwOGP7(+bkBUIDb4g^@Uim>I;eWN7vcg_gP*N^lq6e z`#)G#{dN4T*BkrZ|311?wfPNu$r;sI`JWo%f8Pz+_r*?DKe@raKKk2xKG}oJdt(2u zbM7gb^S%1|bgyq0Z+X0Dn6;*U&EwxQH1+JZUokxV;knhZw;%3B^cTK*y4-AsN!{&_ zkz3`Or-fE6Ik0I$^Ul)K-2$he+kh! z%x)=hcT(tWBc1Ce?>!fJZ+AJoU`yONzqd=>lV3ehV}IFal5CT2w)?){X79t!RR!ku z>f3_(gq}V)(viXcV!6uw!av_N|6do0n3v7ETrW5(HK#qK=*q_XZr3AbMFt;V*H*aO z^Zx%uE{}Hh&0rQ^z_PWD>yz5XZFlrOWy+?D$#|vewR_(#y?*7+^pc+3f;DU_e_PjD z{q?(kWl!$^ACGVGJZ0WF>#~EpmHCUCPnfOBOz(WP-EV%VV#c-m*9~RWYmU5(+y464 znq;XZX)~k$|C#;rhE$!j;N>;@?|t`9o^b4C$@~`_#nDM^Z`Jzeo4>fRf01$di;m2H z3n!MoIeu_`TB&d}r(bDK)(^8Yyr7#!_98aYvmnhL{$Yflm{_w}ean0C)c1M!C(o?? z{@KFPtzLd54`=Z=zNXIeRhoe&7RQWb6}`Uy2+o{(*IMBO^R+wtdT+Pg{k2K4xA0dT z%fA|TNxLNRd4C_J|30zwZdO^{OY0r}$KP4*D|O7f^*^1bqv83LBfI4umz1*9KMHgH zkZ?I{@%NZ3pLe<+-^bm5D*WE^$EEYj)UKqS-t_#LFIVcE{d<}pvge)aF0Xv>+J2g$ z!+XDob+gV|cibxOwYpUHfvfMb=+`Y<%-Q_t|AJmHG-8p9KN&J|t@ieI9< z-}^D&ibI|l$*}PUNc;FJnXO9aeE_=&)Kg|JQaRD<^7h> zSGR5cEuQh!&M)ZR`oi?3e>3XRkJra%ZN6R>$7XzPd0%DKpZ`HwI$Nd{u5?j)32L^_ ze^GJ1@Xf0k<^N0mRQ)|2^S;o8?f9QPeH-KJdtXk=`Q3f5{KbslSFigrojQ5ZmhXM< z^_Z|#)nC=}I9UExS=|d|n_O~c+qv(5?;YR!jW^d=!r(tPk*?^0p(_UP%lUO)K@1CBFzF;_m7zfd^o+FjqD_mAbiOR~#rw>p$^ zN%81=`LD+`cz*@I+FAInMxtJz{nw}9;=c?Amp95a@7qprbkXBILqx6EgV*ez{k|QL zt@?S9+0#@iEG#~9j`SIwpTg^JU;X#NeB!CMMd#Dvb8boI-aG&Ht5k)g;kgqn&Xc(x z$1hTO?ekt%WcS-$b-8+-H|9$lYB%4w?S8GG*ZAVH|8)%px7bT;YRjGE`hWQyi?Az9 z|CG>IqN6Ur-ZOXMTE3mXY+iqDx4FN+@cs3|uceze8|}N9BD=UUS0z5x_FsC*o89j2 z?;m8ovWN~oEMWd(hVq5d&+@N3-itnXeE)0hWOn=g#T_Lzt=o!zl-lgd&-E^u_bQ@C z{Fu-2v`;HoWOpC0FXy;z#kOzS{@1lj%EOQPZw}g=dg5lVOR@UNQ*)ia?ESMOH2ipQ zb=8|{tAFR7?fYVTRj{n+(~gPfubwvLOBDLDwXn1QvSpvMVWscl9yQG#^zQ0d6`lJqPZ6@ztP1{d8wm;7Hu+{1-C;dEQa42!*`aG^r=XxDe zPh6|}dS}IZv(O69#@{V{=~DaRYGu{GZHVOh^w2b{T=u5TEvuxckMDl?PqKdAb1HmM z-@=!(=5c@Dve#^SNv@BoTJEcC<=1~XWq(^0-rRmK>c{P=C2M}4UDum^TZIo)5l0+9 z@~QAdtkpG{*AkB%q(k}dhAquG7Wu37{ElVE_6cl{W-HM6B=yYT+(PRQKQw2R$i!Yg z`!R0)-_60@y{xbJWXqRc<9rwC-?#kl=WX|Q@4M@@|E8QvSkteYHwbQmEdS&^`TXqH z8)9#<&EwwUaP{Apl&MbjS?A?tZeLtqo73^K`hfD?qOOQnS@G>}FO@Az{xKns+&{SgRktd8`1tXJUc7r{Cfq)2M>ErFPQf#qW$Fiyj}a67v$Z(c$KZc{>ACSr2Ee#e!T0l zV3hb0;2slP{_V}SzQoPX9v=v+uQ)P)k9Q*H%|ExM98-`zpVzy1z3$@Z;_unYIp3;s zOO~uRyxL&1_2TR)FJwJpd%yQf@148nb@gJbb!~$^d|^sMei_Hai;q}yh2xFcnttAS ze}1;Z*1mK@_B5#*?s9@Y7MWIilgsz@*YcMYYaRZ%#_{6%;@ftI!mR!8+^RAVeal7D5f zBkzUM?1~Hi_lpZ<=fBIiUCdmX)75iy-OAq5H+JDqZe3$moIS6A&+^07!#~z2_eYv7 zY@0pd?9(57y!ZL8UlrtinCT>-p0n$Y)a>KdH9d##FH8O|m|s3yNp}5up&1o7PoK=O z_;NFNj{O~}cB^kmeOq4HNngBvtl(Z<@18er*VQxdIYb}VxZ{8IliTrUe)DQ`eU9J9 znzSBN2c7G_Y_s0bNs`SccJ=2uD^gBnc5aB3T$MM+^VHilHIbixBzGREtK*PgEwFTL za(&+Sml5ZCOLb<)&6!u@-#bO3y>!jXS0}Dl*Z1u9+WYsqRi3W++ex(>^8P-R&@b(H zWwBVO@HG3qcYLz-b$;t-@5tTzZ)4x$J;wxINKNh&m3{vrc12Q++i}m%4O0}jYj;~c z`&9U~^s}Js<%j#1CHzxl{ZsJw^|7AY^Ipw(R@Qs)u>M|ktD;%u<-WXc=U*s%?r(i< zqv)o@pUvl<=rp`e3prOfXk7ZzDUqF@R5WK@UroMD!3yPf$r;#JG9Y)ye0xuNGuUnJJfAPi zY%^i$H-#KNCX7}#&@Z)bR zZLb*KHLGI#c=GG~*;%qbRhmt6gTywhPS!EqR&%4Le2wzambwC)Z|}1+cz)#Dj9+_C0 z_2I|nGi%=+d#$#_efdNF(k0G++M|_U-_Vaz7WcZm7P0C*}EM zR;Kp&eY5lD73yIhIYpk*TsBGZr2MZYGoup?>r0-UwEDb!)7q3IwOdyUR=BT}l=+`+ z+TFN5|I<5`PnRaLzuPkB@)8~C>J7WxR*cT~CJ7p_e{2`)v6?oMXPaPtM&g zJ;Td1dRLThiH&mQO1{z~pA1)AJ;2QS&mm<|@@)gJW2a0QXUS~&y8F_{`7eLy8}l1f zOh3q6w@P{4%G*{8p0;1FUzBXj^~&SvuS3t1^~<}W^TX|3KeF_f`aHh=@4`vXYvCXN z{J6hJdEf5Gq1%q`P?nu7_vEu_X>oeas~_!LXS3%>u+1}c>t`;JS?@b}+hbGFXWL%x z&Wkv1as4BITA74=X^ZZ9)kh!qa=qLemiTAs+HafYzp~Ie%yC~i;CQ3hipLLcoq0bq z`Z)Gw!9V;`C02Y_ot`k;YrC1#g&7g0zSDbdmtNlU*lr`|g0u-tO?(_4vVFx%MIOwr zl2>2)Z`Iqf+y9o7yj}WLL+!Q4WB)gjU(05!t6rgeJSBHm>W{SBv1y%}-%HQj&t1)5 zn72jj&9V1tv*yK`Z`6JLqi*+&={g#dKjmhY_%QD4V>|wF`Mu*JMFo9dUrYK1?aRMZ zxHJEVqJ#7dzjNnSOKTQSIJMokr}677K%a?|xQidaG;uERSAXxaHrW z%^wr~OzgWNzF&I(e6PNpb%aNr=B^FddVcl$r)=*dZs}Qx?6SLD^(SVZK+oKhlP`UC zsdueE_4<~WTg0AjO|5gwrRHx@eE;U(Uxo#blZszfKe+wx!v*v6U(Yn}ck!3|(8<1U z_2bAhHf%2=_&!!W4p=qY&8n<(-?>`~Gykq!ZuRX~-suCT2Vdznm$RR_*U#wtp^m4n z+}-F+XyUDZ4^Axle~h`Nz3*pFQ+fK5eP4gJTV2|yov-`HK3C!PVqEsc#Ot8_X1+gp(eIUSGry`^Y*a9VDgM3k()ROS zryKjcgYC<^&aL!+;gNadw^2>${cU}djS8kTnR__KH69C;-1qC$(kp-GG?{pA{=d6# z@~6W27YvvGoUV9#scG@$$ae){(nXp7mn3hL<^BG9e*Nygk{bEP^Uvic@0lGCe%wtj z)lxR^_0vtqww!t@k$mjw%(l#EFaM=ye)h61U!OcHMmx>QWOB*HZGF3|-<0NU?aTbD z|I7D;0gW+efwKFB+!hIiGR4 zu5Xm_dKYut`ni3jT7PS;;*Lold6>@AG4t0o_67g{NULAZ>25f?s9N{fzRJD5wY$|5 z_9{<~ee&L`^nA>|&DX_}oxN55h|ym4SJv<7vb}f9D-V4BvDfU=%xB7XuBzqLu6ish^~^#>bW7x#!@EEK zFZ$aqxXWr$+r!VbR^RsgxubuqS8Y$xk2~8lZqJuolI%ab;^1U<{qL&!*>~Mky{;cV z_B#IeT%N_{=2!0hoY{9h?r-*<)n}LJ&y&uW%YOc6_M+qmzxZUV%x;%Ic<|=c*@E14 zin8{Lc#rnf^2awi$UV_5f5Gwi^5%#3&DUxh@4eb>|94mIOIzl}x7~V*@4gFhuPQ&V z?EN8+vW-!eVi<}3U1^||-<|JEgkAHM$j z?v{h6_h$s3?>*mJ@t;}0o~!tx;He9jStW23qsT3tXR-|KcJ&4?d|n+WKV1~u`{l$6 zzKYB1{U2W}vz8a#UjDbMdb#)d_bLCYf1fCw|Nf$&ZNTmCRwl;}l(YUhI={ZXZ>#-_ zb>E}r#7Y0QdN4g%=5J-L?CZx$47#wj0NZMIHc#&>acS21ac8^SlUwV*e=TnAd!XG@ z>tEWF8!r3(y#phELbQ8S@()>-n$FVFHMMo|_Iv)<%Kn!7aPqg&xl$c9VX5-h|8=d( zOl7{s^4Xu6UGw*MdBPs~_uI4m-K~xldj4+d6Fy!4D8T&^+kEN%(w@(iztiX4e)jGD z*9Ymc)y22<>dRl;sP(BeZhZRB_>$nf{~1>8b%mD-KmGB%o*bv0Ran3iG>Pxr!oHXP zF8}fSdV6}w8viHj`ecgV7Oql$_xRbwe=*0OiKpL(uTZi<9<@4O{;g{ zg@pPxdanL`P)5V!eCD=PKjF1%$tAh6cebxi_S4=Se>|}2+^u5c?fx$;PA$zk>iP2F zS&7>sYdm+A75i+-JMbsK-A8p<<#o;Pom>6{R%!J(7JFOO?fq-*o11WO!Ru$f4>qk_ zt$e#!q5P|8@C5q>$!B)%@pYedvwqEEji>fo9^bk5{rB6L{*BAprv;xDJ{0oIQS1ML z`mo)(TeXNfH;HDO>T$Nej1ICC$=LdEmB!=Vj^b&dDua)xL9d za%C0Q%HLL6Zq@f?-TdSi5p(OZZ{9V1v9jyZ;hVBQg14*6zMh*RJDa^-@5c?sa~n;L zeBI~j%;)v{sl```KfLT~m^Vybqip;5O1JQ|+Wxo4t&Y{MIv-7VUg;)tO!>QM#wY)6 z*{7Kivgy4-THX@fmw#E@gDZ^{DjhV>-7H(awDsHPO8e;+$9xX&`*$)r=E%*;4ZptJ zm)+2JQqSbt#!La867Ss?%L;Gb{LS#|S9xj8_SE(Ax!dz*f7}1NuC4Q5bGXBXzV>^6 zJ#AONvko{lljr6y(beY)d`)8x{CAUiaet5HyQlBg9{)3M>W_J?Mo$8cAI^TGHsA1- zhnU9<^UBEnQkyUD{%HCdt}Zt^5i7g#jojbl4J&x#Zfv|Kojl|8_xS>cv3KyKFZU#KFFRgxxyRY* zmd4o`K|6P6t}BiDpC3Q#_3xc7pVnwPuDthOMBLlu>oYUWi{{?q>$fj{Y<790_;dAV zSNCeHeQshqq0jK>6Oj}9`X)Tvskznj&O+t*%QoWIHqP;xb^elQ^+Cg9Gq(0keq1}h z?|QLKRuWB~+AOywan|o2X{UGTe%a%EJYt`S_mgFuUk+U=mVTEq$3bSv_DnCv{IqxP z)~(DF)yX+*YWi*I$(H`>&OYrQ=iR<$Fn#YVp5rdE4+BN{Z_9h_)y*yG33T6}aAfJr z2=`A8dlvN0|NqyTRF`8X7-h8S=&n2>we#2a^jYF-_@Y3_u6^ovD<&&N|9BJ zonNx&oVw>N@ALhiJ74}Pe<5)_o^a~SF}C>Jo}9VTS4M90_57e)xnbvWZf<(VEjQ&u``b zpPa?z_M9_*LH+fJ*=B2PS1dlvvHbp%OD|sE`10}nj4ykWcVEw*`|HVHhO>Fn>|dUj z-r3jOsCGd*fA{2AwcDn9FE@AHsqsmU&(OBs_wvrSpO5!$+&}sM-oDuixw7iNInMWP z)ZcRPPUhXX`70l>yt}k=Xg81Tkie=ZEpS@FAK4+KZjK6Oqj#tU6U--EEW7^6!zOoN* z+im2gAP1YY+Z>XJ+5xzwG+KQj<$!tlJjU^8ee|_rTw&zF^V! zeSCZR*!H~x@5ny(XYTn+mOU3Q8}4UHEcm;)FIHG)?aGbm@$NS3KD_I>{CD}@qs7YQ zb<+QH(-6czsd9JZ`bftbbd;Gohjr{DqsQ>Hq z-~LZ8S+cX`?TKA2nPn!^t3ssetGyS?WHgk%aX(VpQ*!6X-}>adbM;v-Uv*6NW&iBB zaz){W{+B}S&sVs*OUz$pc5%MN)xzCE_m?G4ZtSbbSvTFnw)^PW19~QxKUUqFw_LK^ zKU79>?aJ%b%;$ScTOSx~n$m3P=nrEwf1?FvMQ_l`RKJlq} zZ~Y48JIU`a70&glm*4;4)Z7;w-v z+}9_&rk?w>E7V_M-l`SK?@|nBnmb-EO(`h2(8S|(j_=y-y!F>#eLfbuWa~@O_w|!k zUOg^Jfd6SZ`vD1Q{zqBU73;I=<<+e& zC8i$DZWpOq5OCsm>79s%>4uxnPAd7cYIfbLkAfyv*Wa^0P`q}4c$MbKheSRvv^iHn(+tq!g`)hO$ZFPLjzsH-Y^v}xVWtEHj z-t3CCzf@?QZ9VsPj;pGy`r*TsyRY=$>$&*l^f|r#2e&qvUEiGS_pjiQvemWIXY2mF z-h48JC&MbvO_6V^*?rmiYa73{7mIunZJ&4d`KiO~TkmQc)ZJ2l9qV56=hCW= z?Z?aK@jSbB)i9ew?nkoh>&MgX7c(b6u$yPQMy=lF*DFP(pMUyGebQ@otLJ?Cl{tUS zpSvBUPqs^5$^B}Yi@o!FjOoJKuYyt2j7&DJop)fxoEr<~#7HcCW8Pitd%htr^M@6y z#!`pGdEw^jo8^?JukTQHkQAA5EBv#>|F=Jaxhiard3hZ4vW%$M6ldPD)Av&GC4~!9 zOLDg5UON2l!o<@@UG^q_zF8PkdgPXtwy^9%9+|1wR2M>bBJ(yF9(`#j@4TbJo3BA0F?%@Bb$DRn===eCVIlXum-DUZVNr-&^{u zxudFiZg}pyE#WUH!Jq z{KbjiC;TnAms|09sSvJimN9X?xt^x~{nj=YSs(plFMl?xt1@ZwRZn<)XPu$*0kdHOYxU> zx=c=j2~YB$$%n@tRb&miC8S!N$o+cOw%z%MKHq#z9{2LCu^^$p#wE4dsB(l7LVy?+1s7arMK{Pk;+jn7ukW{Pi_b8O|d z+t232m-dMDyem9nT54W*cZbZQ&T__UU&>$1;48Fe7SDVw@tk+%?~{TTZ5ZAAEmDtP zuPWGf``X=oyFNeTxpP)xuSxQ(17&|3C#PQB7&lw(+l)2JoiVI)bHbP3Ic`wVKl8)w6mow(lC zIr{p|oqhBFO4%M)n7-{s?(OD1dp187JLbOsL+ySGw=eI4I**hrxp4Y~-{RdnpRE10 z&i_-puh$mA#o4KMH|)RrbV1tdODp??>ka04EPVer#_95j)BgW9)JGlfWVd^L|FWT9 zYwq0TR(yMI3-eb*AHQ%d{*gq~|J(148mhHqwpn;wnwZljAbkBwXME+7$F*nHe~D|q z@Rd`RzyJ1Md%fFnU)5!^Uw;X!k9u5p;`!a$BaNnC^DeKN7$<9RdBguTSL(VBZ?(uN zUz7ZH?y~2{cHFftN#Q+QeBgxF#wC*isw(D4F8$hHvLxo;uH3&nHmlEh714jN^Ul{F%dIBaDsAg6iTM>+ z6>{g(ta&Ueo%aiCsefi(nR2BqGqpK;^ZW_&?Gj5G8pF2yOPuw8YrKr!^M?C<)0cU^ z{CRw-Y2}%1eG^iD#7OSjrTdO0b21Od6h_a-&hI|>xJxZD-@YyQo!$jzt7XYPkyE$4 zRj~fL`bGJ5@#F26H?MuHP%I@^cw<-l%ZGnIzRa?@T3Gh$s7dy#+uhZx5^sGAIeySs zblE4qpb5LJf=<=#lFNYWS85|oh!WB$tJ#!Hlc z`YdK=`_;65_R&?h{YsCN%eC}Bx4QOTa>4hkKV>`e#E&1CyQhWY)*odnp(V+G*yHwW zFZS$J&OP^QQD4TJJ9D;w*w?VGul{27qO7fV&p)_R(cafQVR5yxRLRBpcP^{tc|E;z zp}Ak#AXdM87XQqI)pfTyujHPb-?{8bcF)Pff-N&Miq||o(i)$?{d28;t$%6G><5q2 zm}RH)o-a9b>&or9-_CA(wye@FuY5)FHMVE9RhydnCV%cNU330_a;};ByI=bjUHHr> zTmGIQ>f6TC78&*7j`Q@A8_vYvKv*wixM@T!*S=aYAt0>1n>%ne(IG-|x4T{T#Tu-*BmD zveNC1eF?=3xk1LSBKZ5K?!BZq_eI4^wnUHFXJ6z_Klp<0{_b3n|1W&`PMe!;J^0aS zPROEFyYH^6k$!pO^rt7)QT3}d&u(3-ywo{AhP77qp{3q)zv;!@R+kEEbkDmjc@U@s{bA;;QX5Ic6CV}_n-V)|A7Asi|F8oKg&Pd{jo|pZu<|P?Vg|AWnVA&atCpj0Q&=e z1|*WP5EWhe@pF4Eo$b#~lSr6vk-z&V}Rkz%nJ3sE*PcGjp^KR;& zPdQ&!Bu^?=oB8R)3s$zdhn)i6_^@d^$TZNJ*#_vVIq%{weXwj(mQ+Q zA9VL!O%1*@v9@*V^M7S$l9)7o0MKUrUJ}SJ2n? zY1q>Eu)kDg_WN~PV&dH`PGkhEpB*G&(YGRb&+$K3x(^pyU6ae$^Y_3>ukEXrJbs#@ z8vpZJbDz58%ZjfF$Cv%Ok+AZ#v+U>UvRBXN$DHdedGl8Jp0xS-E48wT_7|S>SGDh3 zT%K^DvN`*ebcxUPw-0`l-*04mzpf+x?JE=PM7_It!7L2zm84mea;i&C2IAi z*Zb7`-Fc&Yvu@S(qQ>W^?_}sLf3I+(a_9Qot>rVPT3()5qxWQWzw&~AKUpl4uPUyc zTem`auXTc@`Qs1FiwmzaTP=Ei_y60QmnYVg?fz^(L*aIDY@hF|u=Bmknys!K&Tl&Q z_J;5`mW){Wmp5+f`7``!T>1W~f7GnRj-0!zr6Z45)m$>nv@lFGZ+DR`^zFUfT3?c1 z*?jwdu~pEVTj`dT&vtbC%Krb9^5ygTU4@m$JkQJX`!f70E1k1;<;K_dOP|Ev*~7w6 zRd)P9`i`?d&cBF=w^)*RXWBh}gX(2%?9-2&KO3L<DNoq4(;c~)gy2bX_X@;fBz6<6^EjwWT*{@9V z*xgCFOCGOm+g~VuL9owo+T2Q3*^|#2pWfLeb?@lU-F=g{6lu+@tkXTV?`r$m$m$iz zelfe3t(*Atf$0UqKgpJbzbwzkU-%w!d`SvO7i|mstMd`>hud z=?TyFeHF0Sx~^}-l6!rTrpG?+ey4LM@A}SH-yfM=pV?O$er=;)?0>C)`({g7WiD`$ zh&9hno;&Y%)!ULa-zR#{{q|SrxmDb?dWrtv>DMh{*f#j7K0i|%cX@s4jSn)-!lK^4 zbAuifZhY~|!@Msx_q=+E%#zacB}=mR%N^=+}?invJ3v?m)raO zx?z>7^tPu*uC18<_SZ3^PYaHJte++KCsuM!@8!*X-S_^^pS}Ox?@iO6z5l&K_u>tm zy)*jCH}kMf7kl&CLUVrSxwwk+F5fDz&c7xU__ge>_QyQ&?D>{I=V-j}|5Q0u-pp!R zwBd&S#fw?rt4EwxD3bU#^U|4JadmOC{Z9w{y|8I3Plvp@)gx}Es^oWmd=m3>)`jlB z`yqkx=~5;p+3DY^|2n$wFn^rwdv9TKRB+;siG3ycZ`NAvxYOdue%0mr=WqPhC#~eF z4kX%a+0<8}a{ueo_h&z||2%hn=NI+kQ)^#aEVsI3x2UbK?&ZxD=5BG9U;H#Kd;9bG zX4^kTb)RE?%;kUSao$3Jef`V3AK$-Ke(mwzp~IHlD(?4Diw}RCAI}8|oY-6ax9t0k z*WW#)*4;aPaqq`~<8Q0|^bpHDkhXWQe>m&8Zo(-C%@Y&CX5Gs8(pcJXNhfZGmCMS_ zQ#P2K%zm@&>&%-v`||$%Dxb6Ixf-iv>5|XO=0*Q$v1$|Zxx0MJqr*jYGfvN8U%A}u z!bZEGIdYwvC)%$ozdVsyyG6W4Zew5SgxP@%c9Gt@o*I`MTVCIo7yX;}OJ8yJtB!VF zBfag7e3vh$TU|@c;E?KjC-vN_XinCjPS(^#XQPz9i%M7LX72IbS7B(ru0+)Ss^R9o z(yBK<^}Yp1YM$NvYI;(h+V?Wq{{e0KB|P(IS>+w8cRC&@xudf$V)3V6>3bM%iL<|Z z5*~hh?pfhqNiXdy7+yq$muo&)@%U<5;flxCniwhFo-?YR9LGr@&9KSYa z^RrAuI#J$8=ri{?9xV_orVkN zuUnb@AXzpu_r}}?-~D#4ILr>;jlA^r8~?QZ7vD{1Zp&4Fe(c2u=lCt|da{)~uXY_- zYLp_-VS1FHcxT^R>EowX_I;>hxN|&K((BTW*hNCi z*s`Mz_4=IP*HsyaNeo;GE?>`C@K0hlYpadIlk8Q=|Bmg=D}Cng zsp>TCl3HP2ZT*?7rSGRVDxCA)d$riNSULW`bjgy(_Lc01ecTg1F1U8wWZV2#7TWV8 z{mowgxWE75RnGM#_qNr>`oD~L|9BgZyuXl!srUK?5A)hT{ThwG zl!?RE0?fZ!7V+t`lh3Ko5~3$9UY*ob%n4tyy*oL8hleY-aKf(Fv#uF!aJUdNxzFHV z&_303xsx_TI4|WBcQt?cU`4Xv=e#p@(K0N*CiQt&X*``-%=hMZ>65w?;|2cS-ob}& zYY4CPvvR5`xmEI9T!TqAe0`VjtS6S{=C_wOlt;DQY`kD*8L~z3o|O8}o85hy=hw@~ z-{vbXTJ|{d{+x9l(s9@2w;63ocb?X<|HTfTi$47Idv83yaQCGB5{Bdmi<8%`JPglh z7M$Gob}mnI%=_MgyFHJNJ-@%MZ_2!yeHQ-@Jla#as&QL;NzK8J^Eaee`j@n%3tRnL z{`3l;ckQyZY)_x;vRd}uB;&P$EWhVzv0BEapC81xZ;-spP_3vwp)Y0bjL0~3E2lXE zPrC>YkEqWNynK4fTH^L?N&aBXU-tNNMZD{Ni(?0v zr`FuH6;_$XAuZ9Z_VmsN3G>_k?moN~QTEjEm10@yg#h=8>1>y5@;{~VzqI(^Q2X%T z{;1rDo1E9zUY9RfV;N(X$7K5Zpyu2*`5R{vJD#1IQ|WiSis|et-)OmuANk*>zj$Ra z^H#EzlQiqm&w}f&2Frfj%eDUcfzx-T_1kXp-LLz%;&ICR7fX4!->L9^sB$zvm%Pc^A)UM|`E6|ceeFZfZwp&J zGTpoP-oJIq)BNwq{C6*zqjTWe&2L5ZQ?q*BxiC&BTkSTC;Mcjd&NRZ(VkpHt*k9zQX;d7E*> z@qZOBK5ITYzU{_2t3!7#q>G-N%q)9RA=K}Rhdf@GI%-+6hl4Y08 zIpbMvD)&Cco+skxuEGy@ISY3mFL~pe_gb{x>A3xmKl3jL?wvfFv*zILk{6;17XOO; zW25CS3ziF|wOjH1Ir*h&La^*cw~D=o<3CC~&RzW8I9+zOwdKc#$G2kMC*R6?;Q!A_ z_W!$s=c>Q#>6>izhaPJ^cOt-3@!fnbWNx;L}kGM2F)=PWtB-Bp7*bqY(R=JMl9|30#R@S}ed&ubBZHNTu!>P*>X z@8NHG{bQ*4v1O@$OcsAwxjH!`YreF_pWCH>esw;Vf5~Au*{pQd_W87R(Kq@eURK=HnQLBJrSM8(IrDB0!`-K^h5gUI{ouwHZZal^aqB_i`L6)rft(4J-f#us~*va0vGTlvYIQ&?-$*62SaZC{CV zulf1;8~U@gz8jnOZGRzgyTI4_s-TXe9?@p8zj@M?me*Z|be`|3mrVSKpiCjWg$ zhht?WV%HA4-zdM)EqL40{a$y)y_om*TkiTDUp8IZ_r+|^07~o_7Au)J?DV`zY`*nY-6j`v16bMKDh$miN8g<-g^+7fnC5JNwMa-evy% zo8erB_^*~n^;2iQi0J>%Y~EKatA5@0>icKE*sBVuS&lu7?@wmAs2ba3 zRG?^^63v#!d!?_~w|K^q_UAk*7IFsLOLx}^=`B?#JoYp)DgPEHvSeX?=MV_kzU{uQTK1>pWc54UYqSN z8|2M)&sbKpJo9?)8f9CVgYy!sKKr+-%Me6v7k=bFa~Q|Daz zbA4W-bIgg;A798gN3+%}_bPlep`D*0{sTjU)iT8@t4rn1dzQtmQO<2Q>)!rC;?RZi zH?MC=Bq1h|kb5vlBS#0y|FBmk>|0g0{()}8rKFq8n>O9@IT7jkkK@xv_xf{3_C~%} zo~@p(CRgyKUF7e-yF2GdzLxm4&i3@SnhZ9+m+B=hi>>!FFI=0viQ((F!k}-TLfm_H z&c6S*;D288$LlAj+4VF1t|IU;3UbgZa#0+1*n2`U-zAI9&gqdGbij|MjIYyZ$BmKE1qtWwO=M-TQuZ z_HBp_KYU%=zFyDT?Bl0qc6s?HFQP+j+1@^KL0l_=*5#ffy+k4TmDgOWjoQrHm(@;) zT`=$V{Y$qr&iLTl^y();g4`K~VDdG2#O>j1-&uNG&q0y`s2S9Ury;dv9M`?SvRkP3xz)9`_Nn{#zmQOK_vnq|eE-U$ z+%ziqv1REWh710$)3}d0xL5Qn6{a;0pOZIG! z+x8)@QRKO1?!TM+miSIdO80!o!D4atMthcVa6MJ7C#ns-}t#na!$&VSXbGD-hOfsUw*#hV|aO^`q-)Wr7Oo@duN|%z2@!{ai4xktWW)sAb+`O^G?Nb%Nm9g_uK!iOU_yRwf}@| z;-6HTs`buKLXQ8~TljK@+sjM3%g^5LeSGCr_Af)(+uM7$VsAS&gs@Hc%$IFY+W#p^ zlhx4VU-zXs(>SkWs)pQpRQ@B5i$mVj>n{K8KrW59kB`jT_>E`2=Gm0Znbn8AFRQV( z8An!I^_^00JAZfceC@psSA6-bUP=f*TIDb!W#{R>Dkt|D2cA^Se`OJQ&`q>jqu}zq z#?VVI z#?RgByxNO6*<<;yPyF7o<)}%0%cafpZ@!Ch_i&V5dgNcM`>nj_HvUqXC7-6}1nv7+ z^WxHp?IX<=Dvc_^ihCx93WZhV|6X<(7L|AyL0pQ{_^{*H|f04p4ah(_F4Qz;m7yn&DqP$UE^!vemtmksCD{MY=~ce}5orJNo6Xd6mh^)01=eO{MI5Myq~ z$#v8AOi&a;|MTaG|2mXO(*YEtck->tTQ1b6$~dvK7j;>s~+M z`7yZ@zcZIvr1al>$!`~bP4F38ZQ$%%UM0pE#^(+w^#0~>E?K^*`><8p+g-vPk9q&x z@_(^ouh?w4A17V^`oHeD_AP^f^^EyYeEBuPZm{3%Y#m!}j7i0xWHHpcCnG z%HD>q&&*V7W@m^yzT4`Z$(y&zuPmYp*!V4TWnU)Byfa_A@@!#LC!1p@hyZ7@N>S*zr8OcE;k)ciEpduN3FKs(xx>`##2g&pg?3tJ2G7%rCF}Jy)M^jq)26 zxyu_K*)C9?)_8jF;S#>!3)dOT|GzlL_G|9khDzV1l2;5pk4OI8Fe_PBeV^Q!pEll& za=L}W`y--{|FiRZ!??Qdui>>zr~Zn3`Cln}{##+uV~MrC{4aLs%a>&=faVS^VS(FQ;HhE#=f(H8b?XnAmWJ9CDK zC(~@>(sS`Ird2JT(p{zYf2P0jTiMkPZ-PE(vbGc@R2vFw`fcZNv8_w7HIdoBKKagu z<&UpfInFeB`?I06L9N};Io+pS=E+Z)*Eje*WnOcvJi^R=tj_N2M@x+@7nfRYObB=- zlglprOX>&DgD=NFrdc_0sYRMk%S-&5yUa9?S-iM%@4Y3D6NEYU(Z95Hpr&V%{u)qBlvqeePu(a~IL+0xn z^$ahTef`hJ_}ya1So_23{`ZP<->6q`rjrI9Gw=Xh#%$k*v zt=fw}?i%)$7aH#jmOVNDTj-zKO-44nZ zcph_HF1r4Qu+^boyXzS&_Vyo?koa6F`?LD7n{&>(<1gy+k8Hc{;Qr{t%unSU63H{A zYVu{)ATG~C$?7%1huD@bc*>LG!5DeU{itEm{ZGywjh!o{k0@};1s~$SZBV@JUfzT` zeJ?fbC+wOM6m){=(p5pVecp|h*EdS1l!V+a^gloA&dicE&3f0_wk$n!@Suc1TIc8Q zE_-VgZ0=d?JSn-&TYTU9PR`}CWzMLV%$Xv!Xv&E#@;fG9_`&~rMsnQsE&KlMR9~VV z%%+yw;KcQ4W#7&%k4pcppB>a*((~G4%G^J$hU!*9lAD%Z3A{S#WbWR{Q%{8-{c`53 zvFr}@(lxh!%!w*(xoWU%r{2@m2}Qe&QvBaMQmC9&+LQdHWXbFQ`fEZN9^RZY?{@G7 z3%79hj>&$88~h%WqRvn2t3!tyfHPnYFlA3Z``C2H4@|7lW^B!|Y~WBYbJ=)k*6x^RaQ3T?Y^~~o>xU!e zuRb}y-@NZ1?~2vR_uSN=q<{dmjH@y7_lJ$K|UNkKgFMe|6%z+rQrV ze1CT0$=`zIPfrKBzwkaT@x8D>+wN*%&^{bZ@%NsRP2uKKZF0l%r*9Un()>Bci*2iz z*~`x5sXxzkM_*YR(^i-e+aNL{*Q?IbTxz4F%DJ~kkME!TY+mq&DC6v{d1SoPs$ag3E& z#J=@&qU`={I34n8_PglkR(_Yxtjt{W`1;%V`-4~a7lt2u`&n;E(dEoNN9O97H;2^y zS;rwZ=jyzgAFYq|FU;Dd*?q5X^PSm`(oXxTnyi!W&;MVN-}3qz;f~^F_UFGAesh0P zZHMH)iGLSvkd-t4kjI;|ZMCgxMZ&s9 zg|uydY?nT^IP%th*V1on%kBO0Lj3xV<;R&@m%j=U&0M1FknJoZzxbNn=c~J})jJ-^ zwA^U&C-`yw!5`ip73EH|%4>u6J0F)>{CxF&o*Q<3lP}d}x>T_{SN{>&BKKZXcHxzv za=pwyH#p*sZR;!CJ+W`W*-N~BuiidBKC>@;Hfwma_kiyvR|+jpM8*Vfs} z`vejf|G%{Lzh3XVy`5gakNWzT9{HZ^@__q>e}~KWW$%6)F8YUkK?icPskhB=!-I{N zdusk#3#*xzFNm5PecJlX;~@6wr@|+d*C#U`x~snU{=)Lvf1FJIiOVu<`ugVI#mx@q z`=lek2OPh!ePy!U?^*x$?D+WLhR$r?J*Szuzio>B#r|{h@4|x9%lb2o6ZFqZZ0N7pdvwQx z&&zC|eV;Ywb-z_pd3`76>$mT>rQ79yda(Y}dz%ydFFKA_lpoJt7iw_q#O^t1%+KpT z|7E$jyXWZH!2bO&D(;)FU%9eaTkWOA#|NK{#a>37XFvb{?BDm2vaf58n0{U7d{Mk~ zPL1V^J*OWt%D%j4@K62qjo)(JyXDmqur{6!l>hR#o}MuIRD8~MZ_DK&`|`9{kFVKY zy*Ej#aPBGj%y6qaseLvRJmvU8eY@`8zd8)UcG zE>E62?^^H9*WQ`OPVcOoBX4@a@a>cP!QsIXS0-Ni6PGI8_x;w(8=D;xWm-N7Vyg6DA!>8aafmLzN|m02QszNF<# zM%L?FT5ye;`x`qnVy*6ZziJ1Wi7ifeT4iybzZAL@Dh&z*?vw#$?KAMd&M zEqLekpzRBiukhLiG_sc*slN5@PxQ1MjeQg5?fW8KV{UnEqE~j_wfp<}T<+@o_m;Fg z_W#`W-Xf&T_}J6q-SK`v~&#ktJ=Y86`@81Gtp8e}qKIeAd&Ldl0ZvI^Um51Hoq_!{ou6-gXWv${C)wxL|FPID*Z*F6adP1b z<*8yvr?b00depP*#pw`-*=H@UUf)=kH(%Ghkmp7jXYhXOgzf!_Y>Iy2?tH;!@8r(S zetrLm)Z-cFU!Rc5+miX(qGH9~Gr7Bs>=r(Lu*(pB$qn*ckwPTHJdLg9T89PI87lWp zk((5>vunccDf3fHPMkX_`6I#c$Iic><<5xhdKUS0*WNh8wR&G#9|nuAyQlZ4)4Bbv zUS4(0g*Kz~;FTLw4_h?ewl{7r?kbg8V&rsUsms-873-~nsxlb19GtMBKk~TM)HShL zJzgP8PhOK(U%A-i{hH*tOy{kfUfJj{tVyoj^yr7@Y%8^|wpR`3*V&ub-#!04!hO&2 zKX<et&;V(E#$9lobycWVfE)fx33u^me~!VQg?$zG zcYJ$2zus8(;XY5*O998L?E6knU%dA5(eDN?>4HQaB_EJoZ2keGrY$<6xHqC zZ?pfJ*XEnSv&mBYZ<(U{;qBfkc{O!U8#caQ*_ZP9*4!(Tn?nW9zs#Pqmi@=Ah~Gzd zUr&0od#la%(lzBqXLC)%+Lrx0x$Dc(xjnfBTiDoNzv4f4@2S-j?mf{j7~lHmKIUKg z(Zk*3MBB2XFT~ZukE^U@vnu+zVEyU%!ucM@tL)Y6-mhPE|FvK8E%#5K;`Q(B@Gp7X z@K@p`hk3i>Pt)Z+vtVlhy!1*dUW)&3y!X`VnUDL2<(GC#?|S_7oWb@R;`jIsD%rh) zk1zL9Nt1oObFI}ei-JG(?|#2MwjjG_U3Ap;&wB;?3g2{FiTzEw-^f05-A0qGMMe*A zuuB*K?(c8Mc`j^!qW2;Nm=hRZwAIjSNc-Qjd&ZavJX0P>Ql4E5rKIgxlJLjH{ zd(`aphbKR`|MxuLxQ2AahjgoN`;wnMpZ0qG>o)AQSWU8JGso4$=wGe<7eqJPXr2`f zsx)A&WL_$iYi9Fm+V}n2`zJpRE3et>>h7g||EATBIgYoRFAPC}-LlvhOk{ zk6HJZ(l<-B{`YaNT+Q%aN#(^wiP=1RO8@`WqVS1~;mYm|K%t}WX#`RmU&lFebe(Y^O*fk2VDGjJu%PgOK;dU)ZCx2z*&k4IVoBOQ3`StFRjJ(eF{aARo`>#bC zu70Tc|8~8f{f2_ewV&1E|L;n)Rd~^HUt0bC{!7bjxjm|a;>6YwGg2SCQu`IJs@I8T zzk4g^&%d>Pg093?PIlW5%)uALe}rG~Vmdsr+}@x4*m8>p{!za-vV-pZy1V2OXH_JB zY0o{!S=_Sjuc}`a-I~hycw3eF`uQ8S$WJ-nSs38H#d6}tee)#ml{9yQ; zWnuGEmj64kWActei~628uK8~Gt9j#=OCIh5Lib`fyqK}(^l~e^;y?2*f7}(i=lIp^ zTQ0{hKmPCB{n_KVYKG;p6Y_kiCqCS1sC|-MRm`6EI^w+Ax$27_`H$~kt2}jX*MA4u zhcfK48@F6dbL)S(Bd2udGBde5+xor~FdeV|e{SuvzRb<5%dNh>wY|JAPFVK$Z;#sD z=XZa!|0((W*Byc9#XqZGGw$!7nP&8{i2u|9+l9&da%=8>tKNIA^vtaTAKiOjvpZcX zeE)R6*+XZ(FRl6xnH^7$ZRGp7YQ5$YUaMmtu@B|_I2M%uSBqnY)!t=y3!+@DW9R(3 zI`go(X2IWmDgH0i%9dn0vvDmg3Vsk1wd|hG>xktEWvi=wg8toKs2pWrmOA}}9YgTp zGkeQAj|n{A;v(=i*-Lxb$}hXOmHON|&}ehjaG9v^-A(@GpEaWkeea%mt82xlcOos) z;?l-wp|@Xm?GC%Q@NxL+`}I|K{{F1uE$PWTI>G9!72lSZ(-v~=aXub9tG;K;ndLXj zLxl1ogUxU2W`A{Y=d!x+j8V2&(nV_Bd?x>ICtPQD-fLg_SHt#?TKTVc=kKrcJkBmx zr1t&ojjvK{L7kT;%wV$L=vPv@WT)>N$>0C)995N#JZuqDyY_)G+tSDF=kJ=D9{W4H z@B8h$Jv+WFEvmZvk!@b~rX_x-7|PZPXZkwJ9%M4qnD23Wud};e?#^vQ3@`s-zx8JR zhf4pRt?jk`e}8V&udGzt-SL>$YP;@=hsKfLi{>1j`{vaSW1-U0mXC$EBBEaL@4lgP z(RA}y_Iici z`SX8?@6F=J=l|?_Uv6zD^M6}k`>mXpuOw_&d=QfA`phS*{#yU_j!j=$9?zejscyw| zci-%Cfwi@VkI&!rk=^9ox|RFin#`To_aydwcGin-|ML&{k3V0(ukQ)x@tLLf&mYm} zPL%!l>wT8-Md@d3R%NDp-gDnJyMNwrXO42opX(PN+-2Nx=lJVweX|c=`F$hS?a|kK ztj$!ET?iLXHHdM1a{nt*pYPy*$^Wi-rNIH_xRTv(yLVS)d|CeXM3eocWv|44``J8D zmVG@TVv+c-P0v^L8E?Ak_s3~Pa=yCyfFdjC+;6!~Z#_s;`8UE%N=o%lGY6YcomlnXf!Q{@!VmpCZ*}eQe`Bj_^LHKp za$ok}@AMh3f8_*ie;WL_=w8k}#zw1uYizw=%=ou7Kis{l{6g9P*Z#S(?DwVbFH~N; z^5*LLZ@H#N55C?ecOGkhwjuuvztmD~hwa(VE@YnZjrW<$>~?wOX6I{1EG?Gp-dTEi zt6FM>n&yKU&HYw(hgUteOZ>5oHOL|P?#5u*#FoHQcdJWUME1-mvJ%>~efkWhot9R2 zL+;JF;j*&$Z|$3Dvrf0~*nKxo@BM|sNoir*OG=OYy&rS=xmDfWKc(X7=?63BtzORk z;26`TYld%A_V#;oJy}pE@H)bI>9PygO8)mNT~a8WU*UCJ^~n>)hA#@Tj~9HMU@ZI6 zs%b{WiPyjNUwarbXB6*jk(ncX-0G33==04lFU<8fo&G^HmCt^eGS8)&W&iGq<%nHB zJMWSq`@XrSeM4^Ov9|8~!tWP-JbhnK?z>(?z8f(swmh;fS(455e)jtG5})KR*RI?@ zfBt^|i+dS_6X5Pm2C5-h51+4o^Gm$`-=oHv5#|TvEF_z^X*csEJv&z#{bhm9@gMwh zGnQsw+QH{6eEgxImo4KZ!}m$|Cx`nz6a8~C#4oqt&;5nTR?F^wsx7==vi(-N=li?` zpB(#N{s`^gcx&^Ix!m?o99awNUPwGPjXmDErBr6B8CUG|ZJm8v%jd6~eL~o(Nb<^E zP7kXldl9b?-Meb35}A1kcV%V&2g@er{*%08q4r$jS8?IpU1hKLGd~JBesh-8u0w|J zUd`|d(S2>UzJ7Jz`c1m?X5an&Z6D98m3>E3;-qB{r$_zIkbSvs_D&wjvupp$_+CGI zUdvBn$$R7c=_hPrgCDb8`d};ja@MWAOwX?F3G*xc!x&#Lqx||t%>NGisgAnOesWpO z+b93D{PAzbcwyP!uhpirF8M7VW4^)czt1)Mqz!qOrYB<`b>63WHZyu-dgkir%-y+O zpFA&cP7$>9Jh)8JqC8t*Mcym9izN5i=F3rXFQMRFN|vYs2FklA2Wm8 zu`(aF^{>(*x22q~Xz!bRtIWfm@782Ceuk1?whNQrnRXQKHBX!GDLbjn+j&AC%a`2g z*Jej5Ssnh%w)XMlvr68dOYiHKzRQ$wdS41Uywii*nXk1?Bgc~tJ?cgYQf3HTX*KYo_7AvTc6(-uYB}BZ?Qyq zOX=zKOS8m|TU_0^^5I*FE&Z{w!II~8`gY{6RN) zPpQmWGBf!0L%-m%fO~hVm}bmn3j8Ec$`w*AUM_iH#pBdJ5$8BIFWtRzwZ7fSzS1?9 zqui|ym4&nw-t7xsVDWa(Jcel=d;Tb&?JYTDdEi3XT|U1}hd&wQ=3b*A>|NHb0mfI>6ur`-FRp2kN%&S{c1B zH-NQO_lB^(y^rq4tFv=vnQs-Xzkl(ok?Xz_*1tmQBDQ(o_AC2zD>vX)`jX`0KW}!f z+c@?5?w#PjU+nn$?fa4g>pSHh9Bq+0*8Z5y=hTP9udCJ{{JDKrLVW7u1umLZ zhups#RE`->iIy7={x-a} zZ@0Aj-oACwSFeBczawn2lu-n=?-{q3%)xYj5J)i2Nd=Y3g?dBuEPkDYMX>Xdn{nMavrcS~0I^1ioQubJ`p z%M;&4*|MKytq%DuyJG0|v{%afyI}EhLtCSs`c=stolkG~mB##x?2Ep$%FnU3yU(z6 z!|YwN*dwdsf+g8ku3nS8_t@OmH@>U;ocb{LHOKX1N1hp7`dIg8P8h#tXS>xg(|1mo-;7;z zPSu&NzkOYMiO}+MM;orZx3xO_ektpU&pY<+}bm*h*;mw;7Dt)6CbdEVPaY ze#tmtp5Y~_`kl)zYlhyhFUu~qtjkzjoHgat|0!oV4^-b_wp#YMxAyVh@&%7g)y`bT z+6F=?KPR96{HeTX`NUdAA5Z(I?YkYT3_rHAEH}(ITYKn2nQ*w_?==US*;Q{9{Fy7; zR`TYWl}daySJjJy-m@Fox6X@_+f*$R{9tvte&5aSm0N93Jc)giRblUOb6b@`@`Gi4 z()^#gZB~4}a!sIKtNBa0toyyIivwx?5edyBsTN@#UxZ z-?;U?_Vp#}SM@8u_F%ia=zeUxMgI$1?pWF4%(MIYy4mG?PAq?4>haj$OXbjR>0PR~ z>UMwYxwb<2N6yc=>eBot11UVr|8L>&t>$JFkoU+@t(Kuea3V(uDQ5 z>rT2{GJK=_^YCY$%k}SOpL6_ns7YH}0~!mi)%%!1dhz(jGhO3(sV& z-qrk5yzpz{*{##-HVSTO{C(_+-1S;}x0bD)wXci!RQH^;?k>LF)7UTHe(OvL3v+gO z|H0pemjssI`g7;|?zg-5q${+%y!Y?m?pf!}cl67=w)nu8Z+nu})BR1&%v0xd@4h~5 zW>HR0)_^s}pT_Lq(?b+9@*Ci@HM7me4cZhet_U!eu^}F|NHT%8q$>(){f9{Eu zRR^_|W=S2}x%6XZ-UX5Kx3}JYckfHNtl_gVUsc)lw|&Ldy*8fv9=DQH~u9!9D*3P^gyYucZPL};_D7&7$C;_z8vRO20;g*Xo$G`pJ zUT(2=<^E*7<|&WmU+*}5aE}@PsfMlZd%x<`Z?AQ5uga5LyWjAv$oK!x*WIpX+xf-X zpE3XSj7?pyi>rcOtzE4=_15jFf2*C2=P<4mKM=TbWAJR(tMAw4Z%$Eq*&&sGZGWou zCI!2a{GOF}D&%ut&6pPd=Um~6WG?6L$FjL>mOrpxti0rTv)RNw(zddN^1-kEt?aw2 z-+bpf;cV~z^hj29-d+6`$LPQ}vzdysuLkX&^UljQMtX5Pi<|oF_u(JE`z|Q6)Rt=I zD>9q5<#~id7Au5X3L6luGsA_9Q)#qrM8@uiqD^3+F}+lJHDdt;w_mz<+b&x`wtum zw_8!_{yyE|z1FnGs<`7ao31_6wOV%g@A+39-?#L#{}+_KJnPo?1@?=R|94J5w@d332}ww)G*W?Eiv4^Dk^%{jm0au&id< z+t>B|eM=tVXo5E6`VYy|W8qyH|Is zbk+OT1U?zW!*FpiVOn$2%455y#irG6gR$`|?G8D#xyK)x~=@ z&bcuu)$8TMX6}EyR-ek|G_#m#w>M(=4lJMj--5o4(jyDJ=On|5Ze; z=^pcE!^pw{;k~|@%@~YQ$S3xU+JBtMe(;Cu6WKW zx2=)bkS}X-)iB>pSV{J9P0-ABzYUfsXUzzj2f9m&`$OqwXW7YXmsjS*S<7Zm+q!Y( z!@D&R$5*o|9$PtowgGF+mN2wyr*?eiUX=E_wCd%Z%$QWgqBoYFH!3u`RN7!a@Ba z(|HY3OCQA^zZ4`qU-OM}m4J9XU-g9RMiQkmmju1%Hr(5vxc)cCttB&Sul^PDJb&M@ z!EXLCd(+BwPBLFu;(r~hJH5QK-1a4hUGkNQ_hcDvv0U4?aqDkA#t#SHdwDPHd(g;| zoAL0}@>gF|Y-`vVW&fYuZFH`5jcvo~z5?I9aWCd)zqa_We4q32KYT6A?DfCQeR;zs zuVg=WVPB~W@8b++Sxvn==GS7MJU(v~Xa3rL#~1IEdp;{(c&Dz&So&k3(9(~}a__%> zzEm1;OZ#~4D~`eyHQ{m6YF39NQYT0xFE9VxxhHyE?&b0&k1t)he>~Oq?%I0q%M1Go zx`H#0UJzA3ZpF7R)OJ~N?cF`j$G03aDfMa3UGn(rPyIgr(j!;&u?kN@-Q^Lqb)r7rK^E>31Ewbx8brv}ftvNgTY!&xV= zT=T)zve(PD^n1>}+8oF)@p#F}ua^#A;NNV?c)oN`|Hs?kXYC5}f4lv?u$9xS!c$sv zUT_?)H~n5X{o1iogVPp!p4CkKdvBrg(w)mHC)wvNc+7om*|)uh-{!7aY4I;XzOZ-o zZ`u3O>e;WDtsdFx`b{{s;i%%gUFLGHZ%F5df7=jUCAc=&u>2Fe9yk(@5a^Zx8->o zcmA8QF23D&g~9Gv;UB^5byHK#gy)O@`uzU)-{s%;JzbYy`^UfZ&Q!(f{=W2i1)tcT z8>D}(^E*DTT(9y2+x3X}zF-{XIr!b%n^A z*ZT^y3UYf&)*OHS*x%~d=Y=v%j}v~Hbs1KkbqGIh^6~Vpt+q?|=l!qQ%Vu@x$M13l z#~I(gT3GyR+}CHQBU2v{wEcRr<(-tg=?|M2u=km2p6f(=e9jBgXylpjgE3pIz$$IQ zY`twOCpv_#uwY&JZ_AQ%$7ihMIjib*>Ri*8k`B+*4H@deTq)u1KC)akQ=a*1E|?MK z@^lGX>VsQPS8qGHYr?w!S+8dJX6?|$XCx>V_Ntv7r7hss5drIt@$ zCI9=~sf3G`|CpchzWkt{{c0PJAWK&K|AMC%lWmG8)S9=6$O zv9&5UqFw&Zj>i*x*9IJqU3v6%*<@3`ML|`s!f(FUw>or1sU+8@WJ!Cos$2ct6xr)L z?taSqIb)q|@|9ihXZ8t4pVc@kQQERocG=hYuPUPSetfNRJ8omMW@Z1&8@m;@KmWfV ze|v7xf6$?SJB}PNHT}Nsn8nq?wEn98zNdH0zT5W|zr8NcyjHmIPtoqqzQ3kd3D@Q) z*Zap^dXkz^CGEbiZc}%{?PKXNx#EZ0EaG20-SLB&y)|J{d|RLL8|9yS!s8#O)Z93J z*~q0{GoI!4B>VqT&%6C!h1bg;oNw`A-P_YmvA1SfJuBweQ#@IyHg>V)+y8F$S2oUz zv^f51#%_-#$-n>d7)b38SMHf#zNybzYwM14D_(j3_OSMR|KP^CHxb9P&m6RtSn%Qc z%Dzn3nN!|muJ8Z2r?qtPovy{_N|rDO9DlNNLf=yX>FJp{)y;jAtM9lRf3{ji?cbd} zZ>>xA@CNcYUeDUA`m^Wy$NPo4>mFoTf1G;wx2DJOD{M@2UR&(Q*V|Xt^{Y;nJ^G=c z-BrW=Vb5i)F74)eRQ6Z-Q2)m-f0*C%NxZC>dphCXisTo!W5WAO@A%CA^J|BRd1=q@ zbixCi8K;<{51wH53s`&i#@Bt(e8R8O#SP1zNL{_|a7FAH+rFq(TjzeC_~^>xc5&&J zhc^{(w!NA@xm@^L;iBZXV%2%_k;kiwB9uiQ9}(`%s{JQy6}I@Z-6vCDxf=)V>>lv@ z3)@^Ze4BEgwR(+m%;Snb>|5{cwn~%#e3g0U#h040vq39ijdz;A`@+!}eS7XTj{e%0 zp-MHnk2pSP=H05dXWe#j7T1y5B?hbQ=bQA%|Bl+4y?1%Jc&vTP$H3$NW{cgB=gIz+ zZ1wHWt@^0{4SBL3?o3mfTDyN;mCWlC-{+^7wA9|2weJ7A$4BP-C(n5u(ciy5{nn9} zuR7MBliI`lI^XJ^u>1Tqg|V{#g+6o2e%y7?w3PSAdWYLDEBxoo#?d$b89(E6!R@!( z^|yrnHA`M%5OQwq+|rx=vc9`LuF5Fb%3lsXsrSLO!`bor9oaXhtPbtGcva9_yGW)( zs_(4-&En}zamSySY|r)+m>M=`O2?j=0bdh0Pu{!b(U+N97HwNzX81B9J~>u)r_1B) z1&e}s#tQ)o*S!&qpBz|dY=d~}6uS#T} zMjRLUTy>N0UHz0MU)5V~+y6UiJzmAryUBa=9<#5dM+6I(XWng@@zO4C`_@vQ?Y*6S z@!S8#t)CGUy}UF%^(Wy4v#Q=kZ~yp6;`)qE=N&5_ zKe#J9`BK;NRD+)~`ztoQyeIIuC-47P^XXgPr#(GjbSC!iV}T9Ju0`K%dHK>~>DRK; z-*&a9TRk(~YX5I%-(JIl9kTo{G9(>f8{Lk`Gq^y2T|>TBWXsK06VgO~KDsm0Y|5$9 z+$BLlnzmd9}K5=I$-G_4t;io8GRo|NhL#Ywn-+QXkEX!ztIi7Zl|!{^WQ!``7lvsXZkO zuRh*RmDvA4c=?L5(j_MM18QdBb)Ck! zm3`iS>@WE6zdLs0nCI^Mmp^{E_CxwPqipg|ttj))SJA;$dmwi|V; zld9WHBJ@i_dd$g$_8n`fulUH&-p z@%^iX+m>A`OtXr6@B39^%d&sp)~a8*`jJ1ra3*_SJx?OxvT^5?BlV8^jdG@&?VeV4 zGlG5N9QLWj5&zn5_8jZKeXX#5o$2Qf2|2F=ro>*K+p_bBg+<*Tj?XWrDLV5feOZ3x zvgK97{`$WM*ygV8`@zSf6K_&cen`5+MgIG@U3>SXCoE8o*}nY!0gJ02)vv5yVwBVW zvg6;G&E*aWCpB!A)aG6)xn*_eQAqTM$@VirE%X)2cQ3mw4fS-taq@1p-!!+f;`M>M zt*+gAV|)LruUKh}*n$g}bS_Ri)>3`h@;_r<#l_2mp}|EDZ- zo&P?;S9!kq+WpI|@=kwP*cYy^`*KI+iI#cm)8_2$yMFA~th(;LugTSmd1Atkn=Jm_ zxA3j{n(ZqdzdP+^_-p>XgU++fN`0=2$XqIaVG$jCx#!in70I%j|Hrkyz200lr;35k zSaasiymHQ)b^VQq60{M18D9;<{mCvyT7fJxmA$X5pD|CmeSyf6^>I}v;)6aIZWZD` zcfEGM1>2OcQ*S4W9C#fzVb|W1g$HAgh?scHJ$5DZjQ^_JY%6cyefy|phfDa{WS`CJ zAMKBOe{o{)_j1jQN(T6N=E)5WWT!J6#H-v+pQZ9cv7_ud)h#u3)n ze@pNc-B{!2c>ZNpfz7U`Dao&X1zS0B8QnL_`n$O+<=~Ef$=}bEy^iNDO5PdjAzL@~ zOwjkk$Dh^OW(O}1o%dSe_ACF>vt|X&d;Rd^sRdVVI==cAY*iK3s;lpH z1^=wJdbcmBF;2U9wZ_j&O(S`_0NVHFJJ>yYgUjoWE{L|eUHR9U&UF`po~I`mUD)O#x`g}C@+7VQ zl23$cCl}qkYWSS*+U>xf$0BaO_xaB23VXIJiAQ^!XUB5Y-CpNx#dqmX z#PNT%YocFmiIe=c#wzIe=dbStW!0@MFBV#wJIpqb%V>Uj=iWl)IqzrhyDO9{oU6eq zlgDe`_%8WRU!b4q(a6#_%kC7%doe!W)G&EV-X4Yj()G!Wk>pLCva|aO&m_p-U+_4Y zZ_k038yBt*AzYo`V{~{LQ@8Kk@1y%pG9CJ=?G*k=()_hT!}{WAwxu_xuX+7q?)@3& zm#jHY?f5Fq7$N;_kIllz9*6%3byVF>lictlj#Ve##p=?|rl+g2COx;jpnKEV>Qd=$ z+k3gPr_bJaZl=L3w$)K~z2T#f*-P&#Ew@U_w7>Lh>*a}mE^aWpFzGX!tpD{Z_g}{R zy_LA?xK)$=rx5q3`h&awZr=HSV&DH=a>p%hIXhV*YQg zw!gS_j{MiZ54qLPKG=WPOz*R^>~2+~{g15qSLElse%0~*-GlAwHj9(rHmdKf|6jQ3 z@l1RB<9qq;?>}}+RqF9axhXx-WjdY4JB}6pVK2FJHA?$TH)4$Dr5m6kabb)XuXtOdmFZcJqaqANf z$!>8yamKs%^qogxTRD>+UH`x87w@e{>Z?rW?Aa6YTB0T{g=ywX54+W$oL`)lbGRaS zEWN(qKC@Td+nIgeb6y{}Uzq&K^y|8p_rHB&{$1St@X?(~&$Av(k1lJ@N^zE6ZEJLZhzlv_SNRZ?yvc>xesF5^tIQoEcnA;dggn? zBU!5^`=T%-Pj<*OqTesP?wAjw%w^_Wb;g_qI zC7LZt?jBG+7A*HAI79xjvS0A=*zNoJzLs4n|IXL+*`ieXDB>+sVyWe63Yqvt`b?^op;P<+;PN<$nsyPo!$qRli8La7NKXv!3Z7+B2lsg~v-Eytnwh4Xh!ppb36KU_W@3Fga{J7VbhhHnK zr@oS3`|^R^q1u%6VlHEeFMIm}w?EwM*=2lDQ0;?5=RT7Y&*EZz+r8hqa(n1Mo42`r zb1?S_6+R-`OE2wTG_E@95?f#Q>Fb2^?58(e{PA^9anG*&EumX%J63KHuP|T9BVVQ9 zDE-Odskru(&z4sWr=NH!|9@Yfu>MEa%bn~KOMFy|s*cy2|68S8k?wdr&D9R> zRpkzQR`r##ypORnnf`WPMUCF_3VGx2Z+}0IIDSL?S@B!*s9^TzY*jmF-#hwtcd*Ct z)AwI6+JB#^SIZt3WOd9wbpP(3F~66sti4-a^R@KOw1-EkkA9UnZt!4p%(I|29k#t@ zrln_07rmO1+3`yvS@O)%_zuqZq6>nmZ!-B?UAul(%OU3dD?>HA|79!pzWwR1_F!J$ z;@`5bB;NOi^H;1*-X*3k_Jh4-&d*)H-@jg=E}VQRt^InE%;SLL>m$Ai)m%Gnwd~TY ztL=BTuT@^dTg&@it;Bx%o}0l|-wv<4@0%rCzVzF!$LIh2{A+08m%n>C_Te~%Xok8| z{4-8@th{WfI`zf0gOXFfHD3vQH9N?C+LsAdwsUSwbA0`5!I2%;1u{3+MSa}aH{sd7 zFHKWkGwzUG%Ha6w%InhdI-QB9-X3ZwoKWp$6~fc zUHX$LpZrSgb3Vw&3SF170(jOp80Rb zGP^DPHTi2U-?}XEy5WNHyJXvc8me1&ud@qj>pGoq$j+~%<$2p|$A$VYIj*0zE_vd* zZkE)2>G!v$uUnFA`)|{-yf5zEe0H<0iizmFHNP~`Y|lM&g|#+(=8f5FujYE2+ueI^ zwXJP`l{Kbtw!kPbC z2)35)OkrHZ%l#&G&%fB;%`3M=&pMTOKjzj-^X)gLKRRpBLv%YNoEa{(J$-jS> ztf_Q4o-)_^>+Aon$2TQQybC#Q@{vW%{u5VuaDR*Ul#>*_@&7$_Dw%AB~>8T$5~@^rjECzbi-!Q+|_+N~y;Ehv9gz3Q<>`HKK|3-bc;^32}TA?HiZ zTtBN4pDAfz^0nm5wpn$vo-14N1zkw9y7pKUSH)H!({*^~Pk6V|ve%$?X*YWHu zTXC89qJ7%yR?e~d-_o~ZnNw9kTS-ss>*PCruj~HiUpD+wd-Ztz+IyuI_g*W?UOpUH zTYSVQCG$_x9+5_|=k_-L8A@{3wD`(o=f-w(H?8}3u1$QOnSW7mn%lkO22RD|>PvOgtuDXQSu-T%Ja3b$Qpa^w2P)qkeU z{aj1=-2#*T~8L-BfabzqU8}f;Xpc$prP{$VY!1j~mtOST?Eet3s}<`m>|u zFFMp*`aI=4YNj()9oYYB*_WNGlP!<%7Tot~$1;_9T!W?;@%d^&Ms~g$~8QfA>aRN?s3KV?>WA2`)|H&ch4=( zv{|V9`;_`Eo2AM<3{k$T7VJxw_dow{zfS%8i-B6Wre-!pXvXf0T%@&hs?)PuE3Wt% zpEa4v5xFZj==trZOIGM!=#kuh^f7;>SJ?Efjt&i*!`X3zZVg(aYc zls12+Sj+V(&#kwQm0h?cOwMkuhw0g%?w!4ts@Sc9IyWrq+r4F%>bZq|i@!W6f59R3 z?$YA(rF;I}j5jY&l{{^cvbiy++dlgId1foVKbjF@#Xajy;Kjxh57 zDY_GsdA$6EhwSTez9kkFKAA1We|Imv=i#n3f9CyzLdSR9*ZX$&b%c*XRo&%0n}v@b z>=nCk!s=Vet^V(9vX|f0-~C>F_lN7l%Ra~T0*|Navln`}-+B{r+-`N>$GYzm`n=Cr zFz#5<+*ecm!uI6T@5Uv`1;6bVC%?Dl|8plO-h7AOKC|`OH>MQq>7ICVO|QNGi;5Kw zlP9#z)_qwWvifPhTus;I`3F_{{`e;m9-{duZ|}>mbKPHVhMj`nw}x+BA&X8|KbCzO zwR}T=M*q6k8{GV|_pbl8XCG%q&}-qyR}D?dWdZGfLXOA0-X`}?)@qWyW$yP0eHpR; znm(&JSskj@%g9U6p8GQ5$ey|WjB&w;LLrc9G5eGxy(XjjvZ8STwCK-Ta{KA%E$S-(eC;va7RaILmI__ABj=`Rk7NcRXy_ zavz+JyY|Jmx8Tx_SY?~oOJ8SAJO6QA-&Xdy#~zg1Jux?$QWz(=hjH0G)r_Y7Wvff) zRBli&e6`K$(OvtjzS5lckKfJZ=il=^N#TF7)xGcCwd$w;uPpzkpS1`_*~w`qG$v`cJ!T=BLfOq?~BsW9+t@Vd?U@&kwIzS@ZSd^4cR`Ud)hl$$WN1 z9V0*vdvPtOB035 z>+^oUZ47y}BlzpW$kUQzrt4kRAujn{5yY$ za8aF}I^)EPsNXsJ4>bQ~wc8xhwkn#9qvmOK_v!0rsn#%77Hrf|C-1X)?&XrXzxT~}G!Cd+E zjeVIV7RT(r|IPb-Nb2&{kLr0lWEiT}AD1sV@=^WYm0;QR&(k_~%n2@BXPw*EvU12+SR8!j@~&LxG2`GuHm&?|){3jWvHo?c?e zeNeG{pC;?x8QvK_x)XMt&z#0AYiPfH`nCCP9#+>52mcJXmG@vt<=SeF>%LyHZ6DR1 zo(XV2vDD`DwfHsJ`~Sa)*1UFhUA9(#zvhYSueIdoSUuYseTS!%r`E83OZ9}GEH1LM zlP}n)YyQ}h8}m4$r_}fE(+9zKe$QBc{hj3VU9ml0=ati=Y^G|>D_s-(;riQGl21)b z7d1vExQH4E2uXRmr(yWb}5%k!`I&QJWk%F{q?WyAD8g?K5*=N?~w6o$LF8n!PR@qtiBz(uxI6#XZdsH z#n1fnvZVdIBD?;H6>NXz3Y4rppE)aMw@Qqx(%IaQw)YXoqh{M|{huhyo*)_$Vd1A; z_U!Gh>&Jdo7W{VqXa4(-s~?UoP9(!LA2}~$zm9$zp&%2oa6PjC8S52!+|JHH;+w1nuzQ2YO@>bejDV(%xuG)tAk7MUN+vu6uYFTiz z{OwhL#^u4Y7JNCfTQXGk_AQ@Z^PY*iu{~|^knS?l^xn4QwA?EX%@5UoU%Ld`6ia24!E{ID3-ZTIRDeZl^B3wrYKHZT_ugAMF<<`!AQB!PNJC*{>{> zY8M`BP3P@pYpOafBUsV(<`}3qhO;vlJ z|6OTjiEk5Mw;i|kJ-A#VL%iemwM5^~)Bmdz-qD-?t=mmM`K^Df&53|$jmo%%DKVeg zr^+ANbNGX|N_caqXYc`jxQI6f7oqO0I{KCih!U@^?gX{mv`&9VKsy{s(;BImJce7f*Wd zc3-UQ=JP(Ah01ULC;XM( zo6cCe=J!zxdtcel+*uiS*B$q^EB_<~$08*j=MPP}QpYD0t^V%!-LFS|{5&@Qj{3a$^tM>fecSq`%$s3! zpzf5~x~9!1#aG|@7ZBZ6`ewP&w49*n{iS~*tFI;J=ves53dhQpt9JF3?lE)Z`m~KF zEV8Dgeh{B`YpzP$14 zzrV29xOIuqocik%bL6JT$=%-QqI{!K2 zm$v(#I<(jC{Qml;XaCAg=N!vie)q6$?gNJ3i~IJPP4vfpc0bDc=9`oK)8?Ht=&m%n zpdZ=%@6ORZB3&v1Prbk8{Jexvx={MWAN3vF7P{P$hc-(SDWWh;Gqe~YL)SWRNzlNP(JocqH~ zYl#gmw}t8!+wWm+*|R5f|8?zkD{I(n7A407C+@E4l(&Ckv*E6b`whOnvuk+LI4kXX3zL%=NEU-xp-kwlYWHxwC`3qV(0$*T6O)2 z$6j%v1kuTsvt>_jrQctt`nM|evZ+|LuKj|GFJE6ZcU-&M1;BPu!E)Cq2H> zcPD3cZF671o6_g)%KRze$M{eC9<#~(l5-(w8SyLsL4_Gh)WyZ6f8$gZ`W zP`Oi4He2}9k=;IWF6+8$mObXL5dA#smVIBznh%?5yZfZ||6VGoEq9Pw0=lr_>Nh{d z`IiK3cl`a$Uv)tKg5mm=%(5RP=HHV(T+>&&NB&)T+W8kZOoRU)Wxn_I$@1Xlb{{CwAM*OWslav4lEU*|6EELNTCo51j9~GFHGy|#l*AM*c)VbW zRqXSxi3?>fO*}Or(5gfxf06Q%1^QNt+7fMcn5midZ>|{N<~X z_x>w2{c~R9>W}vq3d7s_-}Cv(YL@=r65na-z`(#3>FMGavMuVyw30hFd!|iTk^HXK z{9e7=%46!XmpNXyhV&b1U9^a(&t!}%(P?+z@kSw9;l@eUZ(9PZEFulw{a(70Cojw7 zh+6cY9r^Q5+Qn`Ab$HX+^Im@qci(;(!*+XNAOBIoiRpxk;rD;muVxqR$@8DN;Kc5k zzpgp&JHUK%-tFwi;(vF(7ytcl<(2rGpSORnx8}RI*#W?)&m?!iuw|$MQU^{_Qo3kjjfne!ebo`ZxJqdF#w|6t7*|I4^pi z*zezsPi4C6EPej7*z|p#{#~c!x%>C|?Wg_;M$6tVe6{W1VyRDe_I}Kf_n*mAZngDt zvQ?+C=3#dmQFoVrr}w9R=9Z3Q zyK{qi#hfdvucf_ZOFdzx`QUZooEILc9c*G9>?Kb`3iQ6TCRpxWeO4i7`uz@ng4$m*%4YhWy(TKvcdqx!?w(!Wcpo>PP2MDb{o`(_XEWBPm#9b| zeKx6Xd!J?6t5~j_! zFQ4!bw|OdqzKVY4*-LCo`!=XVbXC4neY=%s@1L_l){|@JykUQ5=f7g_&7w1jS<14z zm;N=gS)Ba;z}(k2t`wNfU7-9k=l9aRX;%0Ap0ZVyJ9bHJd-F5rWcr-6&wQUBt#A?D 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 literal 0 HcmV?d00001 diff --git a/openharmony-project/entry/src/main/resources/base/media/layered_image.json b/openharmony-project/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000000..fb49920440 --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/openharmony-project/entry/src/main/resources/base/media/startIcon.png b/openharmony-project/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..205ad8b5a8a42e8762fbe4899b8e5e31ce822b8b GIT binary patch literal 20093 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∾Ynw$kjZJzsiSgv#y< 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 Hl+XkKrO}&m literal 0 HcmV?d00001 diff --git a/openharmony-project/entry/src/main/resources/base/profile/backup_config.json b/openharmony-project/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000000..78f40ae7c4 --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/openharmony-project/entry/src/main/resources/base/profile/main_pages.json b/openharmony-project/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000..1898d94f58 --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/openharmony-project/entry/src/main/resources/dark/element/color.json b/openharmony-project/entry/src/main/resources/dark/element/color.json new file mode 100644 index 0000000000..79b11c2747 --- /dev/null +++ b/openharmony-project/entry/src/main/resources/dark/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/openharmony-project/hvigor/hvigor-config.json5 b/openharmony-project/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000..4481d3121f --- /dev/null +++ b/openharmony-project/hvigor/hvigor-config.json5 @@ -0,0 +1,23 @@ +{ + "modelVersion": "6.1.1", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + // "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/openharmony-project/hvigorfile.ts b/openharmony-project/hvigorfile.ts new file mode 100644 index 0000000000..47113e2e36 --- /dev/null +++ b/openharmony-project/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/openharmony-project/oh-package-lock.json5 b/openharmony-project/oh-package-lock.json5 new file mode 100644 index 0000000000..c5a91ec3c9 --- /dev/null +++ b/openharmony-project/oh-package-lock.json5 @@ -0,0 +1,28 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", + "@ohos/hypium@1.0.25": "@ohos/hypium@1.0.25" + }, + "packages": { + "@ohos/hamock@1.0.0": { + "name": "@ohos/hamock", + "version": "1.0.0", + "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har", + "registryType": "ohpm" + }, + "@ohos/hypium@1.0.25": { + "name": "@ohos/hypium", + "version": "1.0.25", + "integrity": "sha512-l6uO2pjl8HyEKdekLqQt7tUpWbDqX/42zoAzkagtUVZAW9jT6lMvbe54MVjoLxq/RwQGygRvi6j4GpypSMFSHw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.25.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/openharmony-project/oh-package.json5 b/openharmony-project/oh-package.json5 new file mode 100644 index 0000000000..01177cc612 --- /dev/null +++ b/openharmony-project/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "modelVersion": "6.1.1", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.25", + "@ohos/hamock": "1.0.0" + } +} diff --git a/src/SDL.c b/src/SDL.c index 0201ad3e17..72c4d77b5f 100644 --- a/src/SDL.c +++ b/src/SDL.c @@ -61,6 +61,9 @@ #ifdef SDL_PLATFORM_ANDROID #include "core/android/SDL_android.h" #endif +#ifdef SDL_PLATFORM_OPENHARMONY +#include "core/openharmony/SDL_openharmony.h" +#endif #define SDL_ALL_SUBSYSTEM_FLAGS ~0U @@ -803,6 +806,8 @@ const char *SDL_GetPlatform(void) return "Nokia N-Gage"; #elif defined(SDL_PLATFORM_OPENBSD) return "OpenBSD"; +#elif defined(SDL_PLATFORM_OPENHARMONY) + return "OpenHarmony"; #elif defined(SDL_PLATFORM_OS2) return "OS/2"; #elif defined(SDL_PLATFORM_OSF) @@ -867,6 +872,8 @@ SDL_FormFactor SDL_GetDeviceFormFactor(void) return SDL_FORMFACTOR_PRIVATE; #elif defined(SDL_PLATFORM_ANDROID) return SDL_GetAndroidDeviceFormFactor(); +#elif defined(SDL_PLATFORM_OPENHARMONY) + return SDL_GetOpenHarmonyDeviceFormFactor(); #elif defined(SDL_PLATFORM_IOS) extern SDL_FormFactor SDL_GetUIKitDeviceFormFactor(void); return SDL_GetUIKitDeviceFormFactor(); diff --git a/src/SDL_log.c b/src/SDL_log.c index 6295fc68ef..fdae94525a 100644 --- a/src/SDL_log.c +++ b/src/SDL_log.c @@ -40,6 +40,10 @@ #include #endif +#ifdef SDL_PLATFORM_OPENHARMONY +#include +#endif + #include "stdlib/SDL_vacopy.h" // The size of the stack buffer to use for rendering log messages. @@ -122,7 +126,19 @@ static int SDL_android_priority[] = { ANDROID_LOG_FATAL }; SDL_COMPILE_TIME_ASSERT(android_priority, SDL_arraysize(SDL_android_priority) == SDL_LOG_PRIORITY_COUNT); -#endif // SDL_PLATFORM_ANDROID +#elif defined(SDL_PLATFORM_OPENHARMONY) +static int SDL_openharmony_priority[] = { + LOG_DEBUG, + LOG_DEBUG, + LOG_DEBUG, + LOG_DEBUG, + LOG_INFO, + LOG_WARN, + LOG_ERROR, + LOG_FATAL +}; +SDL_COMPILE_TIME_ASSERT(openharmony_priority, SDL_arraysize(SDL_openharmony_priority) == SDL_LOG_PRIORITY_COUNT); +#endif static void SDLCALL SDL_LoggingChanged(void *userdata, const char *name, const char *oldValue, const char *hint) { @@ -583,7 +599,7 @@ void SDL_LogMessage(int category, SDL_LogPriority priority, SDL_PRINTF_FORMAT_ST va_end(ap); } -#ifdef SDL_PLATFORM_ANDROID +#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) static const char *GetCategoryPrefix(int category) { if (category < SDL_LOG_CATEGORY_RESERVED2) { @@ -594,7 +610,7 @@ static const char *GetCategoryPrefix(int category) } return "CUSTOM"; } -#endif // SDL_PLATFORM_ANDROID +#endif // SDL_PLATFORM_ANDROID || SDL_PLATFORM_OPENHARMONY void SDL_LogMessageV(int category, SDL_LogPriority priority, SDL_PRINTF_FORMAT_STRING const char *fmt, va_list ap) { @@ -758,12 +774,15 @@ static void SDLCALL SDL_LogOutput(void *userdata, int category, SDL_LogPriority SDL_free(tstr); SDL_small_free(output, isstack); } -#elif defined(SDL_PLATFORM_ANDROID) +#elif defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) { char tag[32]; - SDL_snprintf(tag, SDL_arraysize(tag), "SDL/%s", GetCategoryPrefix(category)); + #if defined(SDL_PLATFORM_ANDROID) __android_log_write(SDL_android_priority[priority], tag, message); + #elif defined(SDL_PLATFORM_OPENHARMONY) + OH_LOG_Print(LOG_APP, SDL_openharmony_priority[priority], LOG_DOMAIN, tag, "%{public}s", message); + #endif } #elif defined(SDL_PLATFORM_APPLE) && (defined(SDL_VIDEO_DRIVER_COCOA) || defined(SDL_VIDEO_DRIVER_UIKIT)) /* Technically we don't need Cocoa/UIKit, but that's where this function is defined for now. diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index 43a71e6532..57121a77da 100644 --- a/src/audio/SDL_audio.c +++ b/src/audio/SDL_audio.c @@ -95,6 +95,9 @@ static const AudioBootStrap *const bootstrap[] = { #ifdef SDL_AUDIO_DRIVER_DOS_SOUNDBLASTER &DOSSOUNDBLASTER_bootstrap, #endif +#ifdef SDL_AUDIO_DRIVER_OHAUDIO + &OHAUDIO_bootstrap, +#endif #ifdef SDL_AUDIO_DRIVER_DISK &DISKAUDIO_bootstrap, #endif diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h index df22b84cbc..76b8443f28 100644 --- a/src/audio/SDL_sysaudio.h +++ b/src/audio/SDL_sysaudio.h @@ -395,5 +395,6 @@ extern AudioBootStrap NGAGEAUDIO_bootstrap; extern AudioBootStrap EMSCRIPTENAUDIO_bootstrap; extern AudioBootStrap QSAAUDIO_bootstrap; extern AudioBootStrap DOSSOUNDBLASTER_bootstrap; +extern AudioBootStrap OHAUDIO_bootstrap; #endif // SDL_sysaudio_h_ diff --git a/src/audio/ohaudio/SDL_ohaudio.c b/src/audio/ohaudio/SDL_ohaudio.c new file mode 100644 index 0000000000..1bfcdd59df --- /dev/null +++ b/src/audio/ohaudio/SDL_ohaudio.c @@ -0,0 +1,648 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +// OpenHarmony OHAudio driver. +// This code is based on the Android AAudio backend, since the two APIs are almost identical. + +#ifdef SDL_AUDIO_DRIVER_OHAUDIO + +#include "../SDL_sysaudio.h" +#include "SDL_ohaudio.h" + +#include +#include + +struct SDL_PrivateAudioData +{ + union { + OH_AudioRenderer *renderer; + OH_AudioCapturer *capturer; + } stream; + + int num_buffers; + Uint8 *mixbuf; // Raw mixing buffer + size_t mixbuf_bytes; // num_buffers * device->buffer_size + size_t callback_bytes; + size_t processed_bytes; + SDL_Semaphore *semaphore; + SDL_AtomicInt error_callback_triggered; +}; + +// !!! FIXME: Remove this LOGI macro. +// Debug +#if 0 +#define LOGI(...) SDL_Log(__VA_ARGS__); +#else +#define LOGI(...) +#endif + +#define LIB_OHAUDIO_SO "libohaudio.so" + +SDL_ELF_NOTE_DLOPEN( + "audio-ohaudio", + "Support for audio through OHAudio", + SDL_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + LIB_OHAUDIO_SO +) + +typedef struct OHAUDIO_Data +{ + SDL_SharedObject *handle; +#define SDL_PROC(ret, func, params) ret (*func) params; +#include "SDL_ohaudiofuncs.h" +} OHAUDIO_Data; +static OHAUDIO_Data ctx; + +static bool OHAUDIO_LoadFunctions(OHAUDIO_Data *data) +{ +#define SDL_PROC(ret, func, params) \ + do { \ + data->func = (ret (*) params)SDL_LoadFunction(data->handle, #func); \ + if (!data->func) { \ + return SDL_SetError("Couldn't load OHAudio function %s: %s", #func, SDL_GetError()); \ + } \ + } while (0); + +#define SDL_PROC_OPTIONAL(ret, func, params) \ + do { \ + data->func = (ret (*) params)SDL_LoadFunction(data->handle, #func); /* if it fails, okay. */ \ + } while (0); +#include "SDL_ohaudiofuncs.h" + return true; +} + + +static const char *AudioStreamResultToString(OH_AudioStream_Result res) +{ + switch (res) { + #define ERRCASE(nam) case AUDIOSTREAM_##nam: return #nam + ERRCASE(SUCCESS); + ERRCASE(ERROR_INVALID_PARAM); + ERRCASE(ERROR_ILLEGAL_STATE); + ERRCASE(ERROR_SYSTEM); + ERRCASE(ERROR_UNSUPPORTED_FORMAT); + #undef ERRCASE + default: break; + } + return "[unknown]"; +} + +static void ErrorCallback(void *userData, OH_AudioStream_Result error) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) userData; + LOGI("SDL ErrorCallback%s: %d - %s", device->recording ? "Capturer" : "Renderer", error, AudioStreamResultToString(error)); + // You MUST NOT close the audio stream from this callback, so we cannot call SDL_AudioDeviceDisconnected here. + // Just flag the device so we can kill it in PlayDevice instead. + SDL_SetAtomicInt(&device->hidden->error_callback_triggered, (int) error); // AUDIOSTREAM_SUCCESS is zero, so !triggered means no error. + SDL_SignalSemaphore(device->hidden->semaphore); // in case we're blocking in WaitDevice. +} + +static void ErrorCallbackCapturer(OH_AudioCapturer *capturer, void *userData, OH_AudioStream_Result error) +{ + ErrorCallback(userData, error); +} + +static void ErrorCallbackRenderer(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Result error) +{ + ErrorCallback(userData, error); +} + +static void ReadDataCallback(OH_AudioCapturer *capturer, void *userData, void *audioData, int32_t numBytes) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) userData; + struct SDL_PrivateAudioData *hidden = device->hidden; + const size_t framesize = SDL_AUDIO_FRAMESIZE(device->spec); + const size_t callback_bytes = (size_t) numBytes; + size_t old_buffer_index = hidden->callback_bytes / device->buffer_size; + const Uint8 *input = (const Uint8 *)audioData; + const size_t available_bytes = hidden->mixbuf_bytes - (hidden->callback_bytes - hidden->processed_bytes); + const size_t size = SDL_min(available_bytes, callback_bytes); + const size_t offset = hidden->callback_bytes % hidden->mixbuf_bytes; + const size_t end = (offset + size) % hidden->mixbuf_bytes; + + SDL_assert(size <= hidden->mixbuf_bytes); + (void) framesize; // only used in macros that bubble out of release code, etc. + + //LOGI("Recorded %zu frames, %zu available, %zu max (%zu written, %zu read)", callback_bytes / framesize, available_bytes / framesize, hidden->mixbuf_bytes / framesize, hidden->callback_bytes / framesize, hidden->processed_bytes / framesize); + + if (offset <= end) { + SDL_memcpy(&hidden->mixbuf[offset], input, size); + } else { + size_t partial = (hidden->mixbuf_bytes - offset); + SDL_memcpy(&hidden->mixbuf[offset], &input[0], partial); + SDL_memcpy(&hidden->mixbuf[0], &input[partial], end); + } + + SDL_MemoryBarrierRelease(); // the other half of this is in OHAUDIO_RecordDevice(). + hidden->callback_bytes += size; + + if (size < callback_bytes) { + LOGI("Audio recording overflow, dropped %zu frames", (callback_bytes - size) / framesize); + } + + // tell the audio thread to pull this data in. + const size_t new_buffer_index = hidden->callback_bytes / device->buffer_size; + while (old_buffer_index < new_buffer_index) { + // Trigger audio processing + SDL_SignalSemaphore(hidden->semaphore); + ++old_buffer_index; + } +} + +static OH_AudioData_Callback_Result WriteDataCallback(OH_AudioRenderer *renderer, void *userData, void *audioData, int32_t numBytes) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) userData; + struct SDL_PrivateAudioData *hidden = device->hidden; + const size_t framesize = SDL_AUDIO_FRAMESIZE(device->spec); + const size_t callback_bytes = (size_t) numBytes; + size_t old_buffer_index = hidden->callback_bytes / device->buffer_size; + Uint8 *output = (Uint8 *)audioData; + const size_t available_bytes = (hidden->processed_bytes - hidden->callback_bytes); + const size_t size = SDL_min(available_bytes, callback_bytes); + const size_t offset = hidden->callback_bytes % hidden->mixbuf_bytes; + const size_t end = (offset + size) % hidden->mixbuf_bytes; + + SDL_assert(size <= hidden->mixbuf_bytes); + (void) framesize; // only used in macros that bubble out of release code, etc. + + //LOGI("Playing %zu frames, %zu available, %zu max (%zu written, %zu read)", callback_bytes / framesize, available_bytes / framesize, hidden->mixbuf_bytes / framesize, hidden->processed_bytes / framesize, hidden->callback_bytes / framesize); + + SDL_MemoryBarrierAcquire(); // the other half of this is in OHAUDIO_PlayDevice(). + if (offset <= end) { + SDL_memcpy(output, &hidden->mixbuf[offset], size); + } else { + size_t partial = (hidden->mixbuf_bytes - offset); + SDL_memcpy(&output[0], &hidden->mixbuf[offset], partial); + SDL_memcpy(&output[partial], &hidden->mixbuf[0], end); + } + hidden->callback_bytes += size; + + if (size < callback_bytes) { + LOGI("Audio playback underflow, missed %zu frames", (callback_bytes - size) / framesize); + SDL_memset(&output[size], device->silence_value, (callback_bytes - size)); + } + + // tell the audio thread to generate more data, so we're ready to memcpy it when this callback runs again. + const size_t new_buffer_index = hidden->callback_bytes / device->buffer_size; + while (old_buffer_index < new_buffer_index) { + // Trigger audio processing + SDL_SignalSemaphore(hidden->semaphore); + ++old_buffer_index; + } + + return AUDIO_DATA_CALLBACK_RESULT_VALID; +} + +static Uint8 *OHAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *bufsize) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + size_t offset = (hidden->processed_bytes % hidden->mixbuf_bytes); + return &hidden->mixbuf[offset]; +} + +static bool OHAUDIO_WaitDevice(SDL_AudioDevice *device) +{ + while (!SDL_GetAtomicInt(&device->shutdown)) { + // this semaphore won't fire when the app is in the background (OHAUDIO_PauseDevices was called). + if (SDL_WaitSemaphoreTimeout(device->hidden->semaphore, 100)) { + return true; // semaphore was signaled, let's go! + } + // Still waiting on the semaphore (or the system), check other things then wait again. + } + return true; +} + +static bool BuildOHAudioStream(SDL_AudioDevice *device); + +static bool RecoverOHAudioDevice(SDL_AudioDevice *device) +{ + SDL_assert(!device->recording); // !!! FIXME: we (currently) only do this for playback devices. This is true in AAudio, too. + + // attempt to build a new stream, in case there's a new default device. + struct SDL_PrivateAudioData *hidden = device->hidden; + ctx.OH_AudioRenderer_Stop(hidden->stream.renderer); + ctx.OH_AudioRenderer_Release(hidden->stream.renderer); + hidden->stream.renderer = NULL; + + SDL_aligned_free(hidden->mixbuf); + hidden->mixbuf = NULL; + + SDL_DestroySemaphore(hidden->semaphore); + hidden->semaphore = NULL; + + const int prev_sample_frames = device->sample_frames; + SDL_AudioSpec prevspec; + SDL_copyp(&prevspec, &device->spec); + + if (!BuildOHAudioStream(device)) { + return false; // oh well, we tried. + } + + // we don't know the new device spec until we open the new device, so we saved off the old one and force it back + // so SDL_AudioDeviceFormatChanged can set up all the important state if necessary and then set it back to the new spec. + const int new_sample_frames = device->sample_frames; + SDL_AudioSpec newspec; + SDL_copyp(&newspec, &device->spec); + + device->sample_frames = prev_sample_frames; + SDL_copyp(&device->spec, &prevspec); + if (!SDL_AudioDeviceFormatChangedAlreadyLocked(device, &newspec, new_sample_frames)) { + return false; // ugh + } + return true; +} + +static bool OHAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + + // WriteDataCallback picks up our work and unblocks OHAUDIO_WaitDevice. But make sure we didn't fail here. + const OH_AudioStream_Result err = (OH_AudioStream_Result) SDL_GetAtomicInt(&hidden->error_callback_triggered); + if (err) { + SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "ohaudio: Audio device triggered error %d (%s)", (int) err, AudioStreamResultToString(err)); + if (!RecoverOHAudioDevice(device)) { + return false; // oh well, we went down hard. + } + } else { + SDL_MemoryBarrierRelease(); + hidden->processed_bytes += buflen; + } + return true; +} + +static int OHAUDIO_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + + // ReadDataCallback picks up our work and unblocks OHAUDIO_WaitDevice. But make sure we didn't fail here. + if (SDL_GetAtomicInt(&hidden->error_callback_triggered)) { + SDL_SetAtomicInt(&hidden->error_callback_triggered, 0); + return -1; + } + + SDL_assert(buflen == device->buffer_size); // If this isn't true, we need to change semaphore trigger logic and account for wrapping copies here + size_t offset = (hidden->processed_bytes % hidden->mixbuf_bytes); + SDL_MemoryBarrierAcquire(); + SDL_memcpy(buffer, &hidden->mixbuf[offset], buflen); + hidden->processed_bytes += buflen; + return buflen; +} + +static void OHAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + + if (hidden) { + if ((device->recording) && (hidden->stream.capturer)) { + ctx.OH_AudioCapturer_Stop(hidden->stream.capturer); + ctx.OH_AudioCapturer_Release(hidden->stream.capturer); + } else if ((!device->recording) && (hidden->stream.renderer)) { + ctx.OH_AudioRenderer_Stop(hidden->stream.renderer); + // !!! FIXME: do we have to wait for the state to change to make sure all buffered audio has played, or will close do this (or will the system do this after the close)? + // !!! FIXME: also, will this definitely wait for a running data callback to finish, and then stop the callback from firing again? + ctx.OH_AudioRenderer_Release(hidden->stream.renderer); + } + + if (hidden->semaphore) { + SDL_DestroySemaphore(hidden->semaphore); + } + + SDL_aligned_free(hidden->mixbuf); + SDL_free(hidden); + device->hidden = NULL; + } +} + +static void SetOptionalStreamUsage(OH_AudioStreamBuilder *builder) +{ + const char *hint = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_ROLE); + if (hint) { + OH_AudioStream_Usage usage = AUDIOSTREAM_USAGE_MOVIE; // !!! FIXME: I don't know if this is a good default. + if ((SDL_strcasecmp(hint, "Communications") == 0) || (SDL_strcasecmp(hint, "GameChat") == 0)) { + usage = AUDIOSTREAM_USAGE_VOICE_COMMUNICATION; + } else if (SDL_strcasecmp(hint, "Game") == 0) { + usage = AUDIOSTREAM_USAGE_GAME; + } + ctx.OH_AudioStreamBuilder_SetRendererInfo(builder, usage); + } +} + +static bool BuildOHAudioStream(SDL_AudioDevice *device) +{ + const char *streamtypestr = device->recording ? "Capturer" : "Renderer"; + + SDL_assert(SDL_GetOpenHarmonySDKVersion() >= 12); // if not, we need to use a different, deprecated way to assign callbacks. It's doable, but messy if not required. + + // !!! FIXME: make sure these all actually settle out into the say channel order SDL expects. + // !!! FIXME: 4POINT1 doesn't match SDL's layout in any case. If you can specify bits instead of prechosen enums, we want CH_LAYOUT_QUAD | CH_SET_LOW_FREQUENCY + static const OH_AudioChannelLayout sdl_layout_map[] = { CH_LAYOUT_MONO, CH_LAYOUT_STEREO, CH_LAYOUT_2POINT1, CH_LAYOUT_QUAD, CH_LAYOUT_4POINT1, CH_LAYOUT_5POINT1_BACK, CH_LAYOUT_6POINT1, CH_LAYOUT_7POINT1 }; + SDL_assert(device->spec.channels <= SDL_arraysize(sdl_layout_map)); // update this if we add support for more channels later. + device->spec.channels = SDL_clamp(device->spec.channels, 1, SDL_arraysize(sdl_layout_map)); + + struct SDL_PrivateAudioData *hidden = device->hidden; + const bool recording = device->recording; + OH_AudioStream_Result res; + + SDL_SetAtomicInt(&hidden->error_callback_triggered, 0); + + OH_AudioStreamBuilder *builder = NULL; + res = ctx.OH_AudioStreamBuilder_Create(&builder, recording ? AUDIOSTREAM_TYPE_CAPTURER : AUDIOSTREAM_TYPE_RENDERER); + if (res != AUDIOSTREAM_SUCCESS) { + LOGI("SDL Failed OH_AudioStreamBuilder_Create %d", (int) res); + return SDL_SetError("SDL Failed OH_AudioStreamBuilder_Create %d", (int) res); + } else if (!builder) { + LOGI("SDL Failed OH_AudioStreamBuilder_Create - builder NULL"); + return SDL_SetError("SDL Failed OH_AudioStreamBuilder_Create - builder NULL"); + } + + OH_AudioStream_SampleFormat format; + if ((device->spec.format == SDL_AUDIO_F32LE) && (SDL_GetOpenHarmonySDKVersion() >= 17)) { + format = AUDIOSTREAM_SAMPLE_F32LE; + } else if (device->spec.format == SDL_AUDIO_S32LE) { + format = AUDIOSTREAM_SAMPLE_S32LE; + } else if (device->spec.format == SDL_AUDIO_S16LE) { + format = AUDIOSTREAM_SAMPLE_S16LE; + } else if (device->spec.format == SDL_AUDIO_U8) { + format = AUDIOSTREAM_SAMPLE_U8; + } else { + format = AUDIOSTREAM_SAMPLE_S16LE; // sint16 is a safe bet for everything else. + } + ctx.OH_AudioStreamBuilder_SetSampleFormat(builder, format); + ctx.OH_AudioStreamBuilder_SetSamplingRate(builder, device->spec.freq); + ctx.OH_AudioStreamBuilder_SetChannelCount(builder, device->spec.channels); + +#if 0 // !!! FIXME: maybe we don't mess with this. (this is from the AAudio backend.) + int32_t sample_frames; + if (SDL_GetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES)) { + sample_frames = device->sample_frames; + } else { + // Use 20 ms for the default audio buffer size + sample_frames = (device->spec.freq / 50); + } + ctx.AAudioStreamBuilder_setBufferCapacityInFrames(builder, 2 * sample_frames); // AAudio requires that the buffer capacity is at least + ctx.AAudioStreamBuilder_setFramesPerDataCallback(builder, sample_frames); // twice the size of the data callback buffer size +#endif + + ctx.OH_AudioStreamBuilder_SetLatencyMode(builder, AUDIOSTREAM_LATENCY_MODE_FAST); + ctx.OH_AudioStreamBuilder_SetChannelLayout(builder, sdl_layout_map[device->spec.channels-1]); + + LOGI("OHAudio Try to generate %s stream %d hz %s %u channels samples %u", + device->recording ? "recording" : "playback", device->spec.freq, + SDL_GetAudioFormatName(device->spec.format), device->spec.channels, + device->sample_frames); + + if (device->recording) { + // try to use a microphone that is for recording external audio, otherwise you might end up with one used for telephone calls right when holding the device near your face. + ctx.OH_AudioStreamBuilder_SetCapturerInfo(builder, AUDIOSTREAM_SOURCE_TYPE_CAMCORDER); + ctx.OH_AudioStreamBuilder_SetCapturerReadDataCallback(builder, ReadDataCallback, device); + // !!! FIXME: we can't seem to bind a stream to a specific device, so I don't see a lot of value in knowing when routing changes. + //OH_AudioStream_Result OH_AudioStreamBuilder_SetCapturerDeviceChangeCallback(OH_AudioStreamBuilder* builder, OH_AudioCapturer_OnDeviceChangeCallback callback, void* userData); + ctx.OH_AudioStreamBuilder_SetCapturerErrorCallback(builder, ErrorCallbackCapturer, device); + res = ctx.OH_AudioStreamBuilder_GenerateCapturer(builder, &hidden->stream.capturer); + } else { + SetOptionalStreamUsage(builder); + ctx.OH_AudioStreamBuilder_SetRendererErrorCallback(builder, ErrorCallbackRenderer, device); + ctx.OH_AudioStreamBuilder_SetRendererWriteDataCallback(builder, WriteDataCallback, device); + // !!! FIXME: we can't seem to bind a stream to a specific device, so I don't see a lot of value in knowing when routing changes. + //OH_AudioStream_Result OH_AudioStreamBuilder_SetRendererOutputDeviceChangeCallback(OH_AudioStreamBuilder* builder, OH_AudioRenderer_OutputDeviceChangeCallback callback, void* userData); + res = ctx.OH_AudioStreamBuilder_GenerateRenderer(builder, &hidden->stream.renderer); + } + + ctx.OH_AudioStreamBuilder_Destroy(builder); + + if (res != AUDIOSTREAM_SUCCESS) { + LOGI("SDL Failed OH_AudioStreamBuilder_Generate%s %d %s", streamtypestr, res, AudioStreamResultToString(res)); + return SDL_SetError("%s : %s", SDL_FUNCTION, AudioStreamResultToString(res)); + } + + // !!! FIXME: it's not clear to me if OHAudio streams will promise you the format you requested, converting if necessary, if Generate(Renderer|Capturer) succeeded. + // !!! FIXME: AAudio goes and queries to see what it _actually_ got from the stream, so we'll do that too. + + // I assume all of these succeed...why would it fail to tell you the sampling rate of the stream, etc? + int32_t sample_frames = 0, freq = 0, channels = 0; + if (device->recording) { + ctx.OH_AudioCapturer_GetFrameSizeInCallback(hidden->stream.capturer, &sample_frames); + ctx.OH_AudioCapturer_GetSamplingRate(hidden->stream.capturer, &freq); + ctx.OH_AudioCapturer_GetChannelCount(hidden->stream.capturer, &channels); + ctx.OH_AudioCapturer_GetSampleFormat(hidden->stream.capturer, &format); + } else { + ctx.OH_AudioRenderer_GetFrameSizeInCallback(hidden->stream.renderer, &sample_frames); + ctx.OH_AudioRenderer_GetSamplingRate(hidden->stream.renderer, &freq); + ctx.OH_AudioRenderer_GetChannelCount(hidden->stream.renderer, &channels); + ctx.OH_AudioRenderer_GetSampleFormat(hidden->stream.renderer, &format); + } + + device->sample_frames = (int) sample_frames; // OHAudio docs claim this is always consistent, never returns something like AAUDIO_UNSPECIFIED like AAudio. + device->spec.freq = (int) freq; + device->spec.channels = (int) channels; + + if (format == AUDIOSTREAM_SAMPLE_U8) { + device->spec.format = SDL_AUDIO_U8; + } else if (format == AUDIOSTREAM_SAMPLE_S16LE) { + device->spec.format = SDL_AUDIO_S16LE; + } else if (format == AUDIOSTREAM_SAMPLE_S32LE) { + device->spec.format = SDL_AUDIO_S32LE; + } else if (format == AUDIOSTREAM_SAMPLE_F32LE) { + device->spec.format = SDL_AUDIO_F32LE; + } else { + return SDL_SetError("Got unexpected audio format %d from OH_Audio%s_GetSampleFormat", (int) format, streamtypestr); + } + + SDL_UpdatedAudioDeviceFormat(device); + + // Allocate a triple buffered mixing buffer + // Two buffers can be in the process of being filled while the third is being read + hidden->num_buffers = 3; + hidden->mixbuf_bytes = (hidden->num_buffers * device->buffer_size); + hidden->mixbuf = (Uint8 *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), hidden->mixbuf_bytes); + if (!hidden->mixbuf) { + return false; + } + hidden->processed_bytes = 0; + hidden->callback_bytes = 0; + + hidden->semaphore = SDL_CreateSemaphore(recording ? 0 : hidden->num_buffers - 1); + if (!hidden->semaphore) { + LOGI("SDL Failed SDL_CreateSemaphore %s recording:%d", SDL_GetError(), recording); + return false; + } + + LOGI("OHAudio Actually generate %s stream %d hz %s %u channels samples %u, buffers %d", + device->recording ? "recording" : "playback", + device->spec.freq, SDL_GetAudioFormatName(device->spec.format), + device->spec.channels, device->sample_frames, hidden->num_buffers); + + return true; +} + +// !!! FIXME: make this non-blocking! +#if 0 +static void SDLCALL RequestAndroidPermissionBlockingCallback(void *userdata, const char *permission, bool granted) +{ + SDL_SetAtomicInt((SDL_AtomicInt *) userdata, granted ? 1 : -1); +} +#endif + +static bool OHAUDIO_OpenDevice(SDL_AudioDevice *device) +{ +#if 0 // !!! FIXME: look up how to do this in HarmonyOS. + if (device->recording) { + // !!! FIXME: make this non-blocking! + SDL_AtomicInt permission_response; + SDL_SetAtomicInt(&permission_response, 0); + if (!SDL_RequestAndroidPermission("android.permission.RECORD_AUDIO", RequestAndroidPermissionBlockingCallback, &permission_response)) { + return false; + } + + while (SDL_GetAtomicInt(&permission_response) == 0) { + SDL_Delay(10); + } + + if (SDL_GetAtomicInt(&permission_response) < 0) { + LOGI("This app doesn't have RECORD_AUDIO permission"); + return SDL_SetError("This app doesn't have RECORD_AUDIO permission"); + } + } +#endif + + device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + return BuildOHAudioStream(device); +} + +static bool PauseOneDevice(SDL_AudioDevice *device, void *userdata) +{ + struct SDL_PrivateAudioData *hidden = (struct SDL_PrivateAudioData *)device->hidden; + if (hidden) { + if (hidden->stream.renderer) { // this is a union, so a valid capture stream will still have a non-NULL `renderer`. + OH_AudioStream_Result res; + if (device->recording) { + res = ctx.OH_AudioCapturer_Pause(hidden->stream.capturer); + } else { + res = ctx.OH_AudioRenderer_Pause(hidden->stream.renderer); + } + + if (res != AUDIOSTREAM_SUCCESS) { + LOGI("SDL Failed OH_Audio%s_Pause %d", device->recording ? "Capturer" : "Renderer", res); + SDL_SetError("%s : %s", SDL_FUNCTION, AudioStreamResultToString(res)); + } + } + } + return false; // keep enumerating. +} + +// Pause (block) all non already paused audio devices by taking their mixer lock +void OHAUDIO_PauseDevices(void) +{ + if (ctx.handle) { // OHAudio driver is used? + (void) SDL_FindPhysicalAudioDeviceByCallback(PauseOneDevice, NULL); + } +} + +// Resume (unblock) all non already paused audio devices by releasing their mixer lock +static bool ResumeOneDevice(SDL_AudioDevice *device, void *userdata) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + if (hidden) { + OH_AudioStream_Result res = AUDIOSTREAM_SUCCESS; + if (device->recording && hidden->stream.capturer) { + res = ctx.OH_AudioCapturer_Start(hidden->stream.capturer); + } else if (!device->recording && hidden->stream.renderer) { + res = ctx.OH_AudioRenderer_Start(hidden->stream.renderer); + } + + if (res != AUDIOSTREAM_SUCCESS) { + LOGI("SDL Failed OH_Audio%s_Start %d", device->recording ? "Capturer" : "Renderer", res); + SDL_SetError("%s : %s", SDL_FUNCTION, AudioStreamResultToString(res)); + } + } + return false; // keep enumerating. +} + +void OHAUDIO_ResumeDevices(void) +{ + if (ctx.handle) { // AAUDIO driver is used? + (void) SDL_FindPhysicalAudioDeviceByCallback(ResumeOneDevice, NULL); + } +} + +static void OHAUDIO_Deinitialize(void) +{ + if (ctx.handle) { + SDL_UnloadObject(ctx.handle); + } + SDL_zero(ctx); +} + + +static void OHAUDIO_ThreadInit(SDL_AudioDevice *device) +{ + SDL_SetCurrentThreadPriority(device->recording ? SDL_THREAD_PRIORITY_HIGH : SDL_THREAD_PRIORITY_TIME_CRITICAL); + // Start the audio stream here; OHAudio's docs say that starting and stopping might take significant time and shouldn't be done from the main thread. + if (device->recording) { + ctx.OH_AudioCapturer_Start(device->hidden->stream.capturer); + } else { + ctx.OH_AudioRenderer_Start(device->hidden->stream.renderer); + } +} + +static bool OHAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + SDL_zero(ctx); + + ctx.handle = SDL_LoadObject(LIB_OHAUDIO_SO); + if (!ctx.handle) { + LOGI("SDL couldn't find " LIB_OHAUDIO_SO); + return false; + } + + if (!OHAUDIO_LoadFunctions(&ctx)) { + SDL_UnloadObject(ctx.handle); + SDL_zero(ctx); + return false; + } + + impl->ThreadInit = OHAUDIO_ThreadInit; + impl->Deinitialize = OHAUDIO_Deinitialize; + impl->OpenDevice = OHAUDIO_OpenDevice; + impl->CloseDevice = OHAUDIO_CloseDevice; + impl->WaitDevice = OHAUDIO_WaitDevice; + impl->PlayDevice = OHAUDIO_PlayDevice; + impl->GetDeviceBuf = OHAUDIO_GetDeviceBuf; + impl->WaitRecordingDevice = OHAUDIO_WaitDevice; + impl->RecordDevice = OHAUDIO_RecordDevice; + impl->HasRecordingSupport = true; + impl->OnlyHasDefaultPlaybackDevice = true; + impl->OnlyHasDefaultRecordingDevice = true; + + LOGI("SDL OHAUDIO_Init OK"); + return true; +} + +AudioBootStrap OHAUDIO_bootstrap = { + "OHAudio", "OpenHarmony OHAudio audio driver", OHAUDIO_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_OHAUDIO diff --git a/src/audio/ohaudio/SDL_ohaudio.h b/src/audio/ohaudio/SDL_ohaudio.h new file mode 100644 index 0000000000..8f8dcaf51c --- /dev/null +++ b/src/audio/ohaudio/SDL_ohaudio.h @@ -0,0 +1,38 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_ohaudio_h_ +#define SDL_ohaudio_h_ + +#ifdef SDL_AUDIO_DRIVER_OHAUDIO + +extern void OHAUDIO_ResumeDevices(void); +extern void OHAUDIO_PauseDevices(void); + +#else + +#define OHAUDIO_ResumeDevices() +#define OHAUDIO_PauseDevices() + +#endif + +#endif // SDL_ohaudio_h_ diff --git a/src/audio/ohaudio/SDL_ohaudiofuncs.h b/src/audio/ohaudio/SDL_ohaudiofuncs.h new file mode 100644 index 0000000000..b09af8ab69 --- /dev/null +++ b/src/audio/ohaudio/SDL_ohaudiofuncs.h @@ -0,0 +1,62 @@ +/* + Simple DirectMedia Layer + Copyright , (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef SDL_PROC_OPTIONAL +#define SDL_PROC_OPTIONAL(ret, func, params) SDL_PROC(ret, func, params) +#endif + +#define SDL_PROC_UNUSED(ret, func, params) + +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_Create, (OH_AudioStreamBuilder** builder, OH_AudioStream_Type type)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_Destroy, (OH_AudioStreamBuilder* builder)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetSamplingRate, (OH_AudioStreamBuilder* builder, int32_t rate)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetChannelCount, (OH_AudioStreamBuilder* builder, int32_t channelCount)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetSampleFormat, (OH_AudioStreamBuilder* builder, OH_AudioStream_SampleFormat format)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetLatencyMode, (OH_AudioStreamBuilder* builder, OH_AudioStream_LatencyMode latencyMode)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetChannelLayout, (OH_AudioStreamBuilder* builder, OH_AudioChannelLayout channelLayout)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetRendererInfo, (OH_AudioStreamBuilder* builder, OH_AudioStream_Usage usage)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetCapturerInfo, (OH_AudioStreamBuilder* builder, OH_AudioStream_SourceType sourceType)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_GenerateRenderer, (OH_AudioStreamBuilder* builder, OH_AudioRenderer** audioRenderer)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_GenerateCapturer, (OH_AudioStreamBuilder* builder, OH_AudioCapturer** audioCapturer)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetRendererWriteDataCallback, (OH_AudioStreamBuilder* builder, OH_AudioRenderer_OnWriteDataCallback callback, void* userData)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetRendererErrorCallback, (OH_AudioStreamBuilder* builder, OH_AudioRenderer_OnErrorCallback callback, void* userData)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetCapturerReadDataCallback, (OH_AudioStreamBuilder* builder, OH_AudioCapturer_OnReadDataCallback callback, void* userData)) +SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetCapturerErrorCallback, (OH_AudioStreamBuilder* builder, OH_AudioCapturer_OnErrorCallback callback, void* userData)) +SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_Release, (OH_AudioRenderer* renderer)) +SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_Start, (OH_AudioRenderer* renderer)) +SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_Pause, (OH_AudioRenderer* renderer)) +SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_Stop, (OH_AudioRenderer* renderer)) +SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_GetSamplingRate, (OH_AudioRenderer* renderer, int32_t* rate)) +SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_GetChannelCount, (OH_AudioRenderer* renderer, int32_t* channelCount)) +SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_GetSampleFormat, (OH_AudioRenderer* renderer, OH_AudioStream_SampleFormat* sampleFormat)) +SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_GetFrameSizeInCallback, (OH_AudioRenderer* renderer, int32_t* frameSize)) +SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_Release, (OH_AudioCapturer* capturer)) +SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_Start, (OH_AudioCapturer* capturer)) +SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_Pause, (OH_AudioCapturer* capturer)) +SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_Stop, (OH_AudioCapturer* capturer)) +SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_GetSamplingRate, (OH_AudioCapturer* capturer, int32_t* rate)) +SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_GetChannelCount, (OH_AudioCapturer* capturer, int32_t* channelCount)) +SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_GetSampleFormat, (OH_AudioCapturer* capturer, OH_AudioStream_SampleFormat* sampleFormat)) +SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_GetFrameSizeInCallback, (OH_AudioCapturer* capturer, int32_t* frameSize)) + +#undef SDL_PROC +#undef SDL_PROC_UNUSED +#undef SDL_PROC_OPTIONAL diff --git a/src/camera/SDL_camera.c b/src/camera/SDL_camera.c index 02b8f530a0..a0cd468fe2 100644 --- a/src/camera/SDL_camera.c +++ b/src/camera/SDL_camera.c @@ -44,6 +44,9 @@ static const CameraBootStrap *const bootstrap[] = { #ifdef SDL_CAMERA_DRIVER_ANDROID &ANDROIDCAMERA_bootstrap, #endif +#ifdef SDL_CAMERA_DRIVER_OPENHARMONY + &OPENHARMONYCAMERA_bootstrap, +#endif #ifdef SDL_CAMERA_DRIVER_EMSCRIPTEN &EMSCRIPTENCAMERA_bootstrap, #endif diff --git a/src/camera/SDL_syscamera.h b/src/camera/SDL_syscamera.h index 40c4ae9fe8..4b4c5be45c 100644 --- a/src/camera/SDL_syscamera.h +++ b/src/camera/SDL_syscamera.h @@ -222,6 +222,7 @@ extern CameraBootStrap PIPEWIRECAMERA_bootstrap; extern CameraBootStrap V4L2_bootstrap; extern CameraBootStrap COREMEDIA_bootstrap; extern CameraBootStrap ANDROIDCAMERA_bootstrap; +extern CameraBootStrap OPENHARMONYCAMERA_bootstrap; extern CameraBootStrap EMSCRIPTENCAMERA_bootstrap; extern CameraBootStrap MEDIAFOUNDATION_bootstrap; extern CameraBootStrap VITACAMERA_bootstrap; diff --git a/src/camera/openharmony/SDL_camera_openharmony.c b/src/camera/openharmony/SDL_camera_openharmony.c new file mode 100644 index 0000000000..12fe3cdd5b --- /dev/null +++ b/src/camera/openharmony/SDL_camera_openharmony.c @@ -0,0 +1,775 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "../SDL_syscamera.h" +#include "../SDL_camera_c.h" +#include "../../video/SDL_pixels_c.h" +#include "../../video/SDL_surface_c.h" +#include "../../core/openharmony/SDL_openharmony.h" + +#ifdef SDL_CAMERA_DRIVER_OPENHARMONY + +/* + * module.json5: + * { "name": "ohos.permission.CAMERA", "reason": "$string:perm_reason_CAMERA", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } }, + * + * Very likely SDL must be build with YUV support (done by default) + */ + +// !!! FIXME: Using a "preview output" is the only way I could find to read pixels on the CPU. If I +// !!! FIXME: try to use a "video output" with an image receiver, it doesn't work: I get data from +// !!! FIXME: the camera and I can see motion in the output if I wave my hand in front of the camera, +// !!! FIXME: but it's _definitely_ not the NV21 format it claims to be, and the image is some weird +// !!! FIXME: scrambled thing split into rows. +// !!! FIXME: While preview output is "fine," it doesn't give us access to various camera framerates +// !!! FIXME: (we currently lie and say 60fps), nor the maximum resolution of the camera. +// !!! FIXME: I think VideoOutput wants the system to control the whole pipeline, from lens to encoder +// !!! FIXME: to disk, and preview output wants to add the GPU into the mix but will accept CPU access +// !!! FIXME: if you want to do things like Snapchat filters or whatever on the data. I don't know. + +// prevent unneeded rawfile headers (which contain C++ code) from getting included by other system headers. +typedef struct RawFileDescriptor RawFileDescriptor; +#define GLOBAL_RAW_FILE_H 1 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/openharmony/SDL_openharmony.h" + +struct SDL_PrivateCameraData +{ + Camera_Input *input; + OH_ImageReceiverOptions *options; + OH_ImageReceiverNative *image_receiver; + Camera_PreviewOutput *output; + //Camera_VideoOutput *output; + Camera_CaptureSession *session; + int rotation; // degrees to rotate clockwise to get from camera's static orientation to device's native orientation. Apply this plus current phone rotation to get upright image! + SDL_CameraSpec requested_spec; +}; + +static bool SetErrorStr(const char *what, const char *errstr, const int rc) +{ + char errbuf[128]; + if (!errstr) { + SDL_snprintf(errbuf, sizeof (errbuf), "Unknown error #%d", rc); + errstr = errbuf; + } + return SDL_SetError("%s: %s", what, errstr); +} + +static const char *CameraErrorStr(const Camera_ErrorCode rc) +{ + switch (rc) { + case CAMERA_OK: return "no error"; + case CAMERA_INVALID_ARGUMENT: return "invalid argument"; + case CAMERA_OPERATION_NOT_ALLOWED: return "operation not allowed"; + case CAMERA_SESSION_NOT_CONFIG: return "session not configured"; + case CAMERA_SESSION_NOT_RUNNING: return "session not running"; + case CAMERA_SESSION_CONFIG_LOCKED: return "session config locked"; + case CAMERA_DEVICE_SETTING_LOCKED: return "device setting locked"; + case CAMERA_CONFLICT_CAMERA: return "camera conflict"; + case CAMERA_DEVICE_DISABLED: return "device disabled"; + case CAMERA_DEVICE_PREEMPTED: return "device preempted"; + case CAMERA_UNRESOLVED_CONFLICTS_WITH_CURRENT_CONFIGURATIONS: return "unresolved conflicts with current configuration"; + case CAMERA_SERVICE_FATAL_ERROR: return "fatal error"; + default: break; + } + + return NULL; // unknown error +} + +static bool SetCameraError(const char *what, const Camera_ErrorCode rc) +{ + return SetErrorStr(what, CameraErrorStr(rc), (int) rc); +} + +static const char *ImageErrorStr(const Image_ErrorCode rc) +{ + switch (rc) { + case IMAGE_SUCCESS: return "no error"; + case IMAGE_BAD_PARAMETER: return "bad parameter"; + case IMAGE_UNSUPPORTED_MIME_TYPE: return "unsupported mimetype"; + case IMAGE_UNKNOWN_MIME_TYPE: return "unknown mimetype"; + case IMAGE_TOO_LARGE: return "too large"; + case IMAGE_GET_IMAGE_DATA_FAILED: return "get image data failed"; + case IMAGE_DMA_NOT_EXIST: return "DMA doesn't exist"; + case IMAGE_DMA_OPERATION_FAILED: return "DMA operation failed"; + case IMAGE_UNSUPPORTED_OPERATION: return "unsupported operation"; + case IMAGE_UNSUPPORTED_METADATA: return "unsupported metadata"; + case IMAGE_UNSUPPORTED_CONVERSION: return "unsupported conversion"; + case IMAGE_INVALID_REGION: return "invalid region"; + case IMAGE_UNSUPPORTED_MEMORY_FORMAT: return "unsupported memory format"; + case IMAGE_INVALID_PARAMETER: return "invalid parameter"; + case IMAGE_UNSUPPORTED_DATA_FORMAT: return "unsupported data format"; + case IMAGE_ALLOC_FAILED: return "allocation failed"; + case IMAGE_COPY_FAILED: return "copy failed"; + case IMAGE_LOCK_UNLOCK_FAILED: return "unlock failed"; + case IMAGE_INIT_FAILED: return "init failed"; + case IMAGE_CREATE_PIXELMAP_FAILED: return "create pixelmap failed"; + case IMAGE_ALLOCATOR_MODE_UNSUPPORTED: return "allocator mode unsupported"; + case IMAGE_UNKNOWN_ERROR: return "unknown error"; + case IMAGE_BAD_SOURCE: return "bad source"; + case IMAGE_SOURCE_UNSUPPORTED_MIME_TYPE: return "unsupported source mimetype"; + case IMAGE_SOURCE_TOO_LARGE: return "source too large"; + case IMAGE_SOURCE_UNSUPPORTED_ALLOCATOR_TYPE: return "unsupported source allocator type"; + case IMAGE_SOURCE_UNSUPPORTED_METADATA: return "unsupported source metadata"; + case IMAGE_SOURCE_UNSUPPORTED_OPTIONS: return "unsupported source options"; + case IMAGE_SOURCE_INVALID_PARAMETER: return "invalid source parameter"; + case IMAGE_DECODE_FAILED: return "decode failed"; + case IMAGE_SOURCE_ALLOC_FAILED: return "source allocation failed"; + case IMAGE_PACKER_INVALID_PARAMETER: return "invalid packer parameter"; + case IMAGE_ENCODE_FAILED: return "encode failed"; + case IMAGE_RECEIVER_INVALID_PARAMETER: return "invalid receiver parameter"; + default: break; + } + + return NULL; // unknown error +} + +static bool SetImageError(const char *what, const Image_ErrorCode rc) +{ + return SetErrorStr(what, ImageErrorStr(rc), (int) rc); +} + + +static Camera_Manager *camera_manager = NULL; + +static bool CreateCameraManager(void) +{ + SDL_assert(camera_manager == NULL); + const Camera_ErrorCode rc = OH_Camera_GetCameraManager(&camera_manager); + if (rc != CAMERA_OK) { + return SetCameraError("Error creating Camera_Manager", rc); + } + return true; +} + +static void DestroyCameraManager(void) +{ + if (camera_manager) { + OH_Camera_DeleteCameraManager(camera_manager); + camera_manager = NULL; + } +} + +static void ConvertOpenHarmonyFormatToSDL(Camera_Format fmt, SDL_PixelFormat *format, SDL_Colorspace *colorspace) +{ + switch (fmt) { + #define CASE(x, y, z) case x: *format = y; *colorspace = z; return + CASE(CAMERA_FORMAT_YUV_420_SP, SDL_PIXELFORMAT_NV21, SDL_COLORSPACE_BT709_LIMITED); + CASE(CAMERA_FORMAT_JPEG, SDL_PIXELFORMAT_MJPG, SDL_COLORSPACE_SRGB); + CASE(CAMERA_FORMAT_RGBA_8888, SDL_PIXELFORMAT_RGBA8888, SDL_COLORSPACE_SRGB); + #undef CASE + default: break; + } + + #if DEBUG_CAMERA + SDL_Log("CAMERA: Unknown format Camera_Format '%d'", fmt); + #endif + + *format = SDL_PIXELFORMAT_UNKNOWN; + *colorspace = SDL_COLORSPACE_UNKNOWN; +} + +static Camera_Format ConvertSDLFormatToOpenHarmony(SDL_PixelFormat fmt) +{ + switch (fmt) { + #define CASE(x, y) case y: return x + CASE(CAMERA_FORMAT_YUV_420_SP, SDL_PIXELFORMAT_NV21); + CASE(CAMERA_FORMAT_JPEG, SDL_PIXELFORMAT_MJPG); + CASE(CAMERA_FORMAT_RGBA_8888, SDL_PIXELFORMAT_RGBA8888); + #undef CASE + default: return 0; + } +} + +static bool FindOpenHarmonyCameraByDevicePtr(SDL_Camera *device, void *userdata) +{ + const Camera_Device *ohdevice = (const Camera_Device *) userdata; + return (SDL_strcmp(ohdevice->cameraId, ((const Camera_Device *) device->handle)->cameraId) == 0); +} + +static bool FindOpenHarmonyCameraByImageReceiver(SDL_Camera *device, void *userdata) +{ + OH_ImageReceiverNative *receiver = (OH_ImageReceiverNative *) userdata; + struct SDL_PrivateCameraData *hidden = device->hidden; + return (hidden && (hidden->image_receiver == receiver)); +} + +static bool FindOpenHarmonyCameraByInput(SDL_Camera *device, void *userdata) +{ + Camera_Input *input = (Camera_Input *) userdata; + struct SDL_PrivateCameraData *hidden = device->hidden; + return (hidden && (hidden->input == input)); +} + +static bool OPENHARMONYCAMERA_WaitDevice(SDL_Camera *device) +{ + return true; // this isn't used atm, since we run our own thread via onImageAvailable callbacks. +} + +static SDL_CameraFrameResult OPENHARMONYCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, float *rotation) +{ + struct SDL_PrivateCameraData *hidden = device->hidden; + SDL_CameraFrameResult result = SDL_CAMERA_FRAME_READY; + Image_ErrorCode imgrc = IMAGE_SUCCESS; + + OH_ImageNative *image = NULL; + if ((imgrc = OH_ImageReceiverNative_ReadNextImage(hidden->image_receiver, &image)) != IMAGE_SUCCESS) { + SetImageError("Failed to read next image", imgrc); + return SDL_CAMERA_FRAME_ERROR; + } + + Image_Size size; + if ((imgrc = OH_ImageNative_GetImageSize(image, &size)) != IMAGE_SUCCESS) { + SetImageError("Failed to get image size", imgrc); + OH_ImageNative_Release(image); + return SDL_CAMERA_FRAME_ERROR; + } + + int64_t ohtimestamp = 0; + if (OH_ImageNative_GetTimestamp(image, &ohtimestamp) == IMAGE_SUCCESS) { + *timestampNS = (Uint64) ohtimestamp; + } else { + *timestampNS = 0; + } + + uint32_t components_buf[16]; + uint32_t *components = components_buf; + size_t num_components = SDL_arraysize(components_buf); + if ((imgrc = OH_ImageNative_GetComponentTypes(image, &components, &num_components)) != IMAGE_SUCCESS) { + SetImageError("Failed to get image component types", imgrc); + OH_ImageNative_Release(image); + return SDL_CAMERA_FRAME_ERROR; + } + SDL_assert(num_components <= SDL_arraysize(components_buf)); // just in case. + + int num_planes = (int) num_components; + +#if 0 +SDL_Log("CAMERA: %d planes:", (int) num_components); +for (size_t i = 0; i < num_components; i++) { SDL_Log("CAMERA: - %d: %d", (int) i, (int) components[i]); } + + // !!! FIXME: this currently copies the data to the surface (see FIXME about non-contiguous planar surfaces, but in theory we could just keep this locked until ReleaseFrame... + if ((num_planes == 3) && (device->spec.format == SDL_PIXELFORMAT_NV21)) { +SDL_Log("THIS IS AN NV21 IMAGE, ALLEGEDLY."); + num_planes--; // treat the interleaved planes as one. + } +#endif + + int32_t stride = 0; + // !!! FIXME: this works because of luck of how YUV420 and NV21 are layed out, but we _should_ check this for each component. + if ((imgrc = OH_ImageNative_GetRowStride(image, components[0], &stride)) != IMAGE_SUCCESS) { + SetImageError("Failed to get image row stride", imgrc); + OH_ImageNative_Release(image); + return SDL_CAMERA_FRAME_ERROR; + } + + frame->pitch = (int) stride; + + size_t buflen = 0; + for (int i = 0; (i < num_planes) && (i < 3); i++) { + size_t datalen = 0; + OH_ImageNative_GetBufferSize(image, components[i], &datalen); + buflen += datalen; + } + + frame->pixels = SDL_aligned_alloc(SDL_GetSIMDAlignment(), buflen); + if (frame->pixels == NULL) { + result = SDL_CAMERA_FRAME_ERROR; + } else { + Uint8 *dst = frame->pixels; + + for (int i = 0; i < num_planes; i++) { + OH_NativeBuffer *imagebuf = NULL; + OH_ImageNative_GetByteBuffer(image, components[i], &imagebuf); + + size_t datalen = 0; + OH_ImageNative_GetBufferSize(image, components[i], &datalen); + + void *data = NULL; + if (OH_NativeBuffer_Map(imagebuf, &data) != 0) { + SDL_memset(dst, 0, datalen); // oh well. + } else { + SDL_memcpy(dst, data, datalen); + OH_NativeBuffer_Unmap(imagebuf); + } + + dst += datalen; + } + } + + OH_ImageNative_Release(image); + + int dev_rotation = 0; + switch (SDL_GetOpenHarmonyDeviceCurrentOrientation()) { + case SDL_ORIENTATION_PORTRAIT: dev_rotation = 0; break; + case SDL_ORIENTATION_LANDSCAPE: dev_rotation = 90; break; + case SDL_ORIENTATION_PORTRAIT_FLIPPED: dev_rotation = 180; break; + case SDL_ORIENTATION_LANDSCAPE_FLIPPED: dev_rotation = 270; break; + default: SDL_assert(!"Unexpected device rotation!"); dev_rotation = 0; break; + } + + if (device->position == SDL_CAMERA_POSITION_BACK_FACING) { + dev_rotation = -dev_rotation; // we want to subtract this value, instead of add, if back-facing. + } + + *rotation = (float) (dev_rotation + device->hidden->rotation); // current phone orientation, static camera orientation in relation to phone. + + return result; +} + +static void OPENHARMONYCAMERA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame) +{ + // !!! FIXME: this currently copies the data to the surface, but in theory we could just keep the OH_ImageNative until ReleaseFrame... + SDL_aligned_free(frame->pixels); +} + +static void OnImageReceiverFrame(OH_ImageReceiverNative *receiver) +{ + #if DEBUG_CAMERA + SDL_Log("CAMERA: CB OnImageReceiverFrame"); + #endif + SDL_Camera *device = SDL_FindPhysicalCameraByCallback(FindOpenHarmonyCameraByImageReceiver, receiver); + if (device) { + SDL_CameraThreadIterate(device); + } +} + +static void OnVideoOutputFrameStart(Camera_PreviewOutput *output) +//static void OnVideoOutputFrameStart(Camera_VideoOutput *output) +{ + #if DEBUG_CAMERA + SDL_Log("CAMERA: CB OnVideoOutputFrameStart"); + #endif +} + +static void OnVideoOutputFrameEnd(Camera_PreviewOutput *output, int32_t frame_count) +//static void OnVideoOutputFrameEnd(Camera_VideoOutput *output, int32_t frame_count) +{ + #if DEBUG_CAMERA + SDL_Log("CAMERA: CB OnVideoOutputFrameEnd"); + #endif +} + +static void OnVideoOutputError(Camera_PreviewOutput *output, Camera_ErrorCode err) +//static void OnVideoOutputError(Camera_VideoOutput *output, Camera_ErrorCode err) +{ + #if DEBUG_CAMERA + SDL_Log("CAMERA: CB OnVideoOutputError"); + #endif +} + +static void OnCameraInputError(const Camera_Input *input, Camera_ErrorCode err) +{ + #if DEBUG_CAMERA + SDL_Log("CAMERA: CB OnCameraInputError"); + #endif + SDL_Camera *device = SDL_FindPhysicalCameraByCallback(FindOpenHarmonyCameraByInput, (void *) input); + if (device) { + SDL_CameraDisconnected(device); + } +} + +static void OPENHARMONYCAMERA_CloseDevice(SDL_Camera *device) +{ + if (device && device->hidden) { + struct SDL_PrivateCameraData *hidden = device->hidden; + + if (hidden->session) { + OH_CaptureSession_Stop(hidden->session); + OH_CaptureSession_Release(hidden->session); + } + + if (hidden->input) { + OH_CameraInput_Close(hidden->input); + OH_CameraInput_Release(hidden->input); + } + + if (hidden->output) { + OH_PreviewOutput_Stop(hidden->output); + OH_PreviewOutput_Release(hidden->output); + //OH_VideoOutput_Stop(hidden->output); + //OH_VideoOutput_Release(hidden->output); + } + + if (hidden->image_receiver) { + OH_ImageReceiverNative_Release(hidden->image_receiver); + } + + if (hidden->options) { + OH_ImageReceiverOptions_Release(hidden->options); + } + + device->hidden = NULL; + + SDL_free(hidden); + } +} + +// this is where the "opening" of the camera happens, after permission is granted. +static bool PrepareCamera(SDL_Camera *device) +{ + struct SDL_PrivateCameraData *hidden = device->hidden; + SDL_assert(hidden != NULL); + + // just in case SDL_OpenCamera is overwriting device->spec as CameraPermissionCallback runs, we work from a different copy. + const SDL_CameraSpec *spec = &hidden->requested_spec; + + Camera_ErrorCode rc = CAMERA_OK; + Image_ErrorCode imgrc = IMAGE_SUCCESS; + Camera_Device *ohdevice = (Camera_Device *) device->handle; + + const Camera_Profile video_profile = { ConvertSDLFormatToOpenHarmony(spec->format), { (uint32_t) spec->width, (uint32_t) spec->height } }; + //const Camera_VideoProfile video_profile = { ConvertSDLFormatToOpenHarmony(spec->format), { (uint32_t) spec->width, (uint32_t) spec->height }, { spec->framerate_numerator, spec->framerate_numerator } }; + const Image_Size imgsize = { (uint32_t) spec->width, (uint32_t) spec->height }; + const int32_t imgcapacity = 8; // image buffer queue size. docs recommend this be 8: https://developer.huawei.com/consumer/en/doc/harmonyos-guides/native-camera-preview-imagereceiver + uint64_t receiver_surface_id = 0; + char receiver_surface_id_str[64]; + uint32_t orientation = 0; + + CameraInput_Callbacks camera_input_callbacks = { OnCameraInputError }; + PreviewOutput_Callbacks video_output_callbacks = { OnVideoOutputFrameStart, OnVideoOutputFrameEnd, OnVideoOutputError }; + //VideoOutput_Callbacks video_output_callbacks = { OnVideoOutputFrameStart, OnVideoOutputFrameEnd, OnVideoOutputError }; + + if ((imgrc = OH_ImageReceiverOptions_Create(&hidden->options)) != IMAGE_SUCCESS) { + return SetImageError("Failed to create image receiver options", imgrc); + } else if ((imgrc = OH_ImageReceiverOptions_SetSize(hidden->options, imgsize)) != IMAGE_SUCCESS) { + return SetImageError("Failed to set image receiver size option", imgrc); + } else if ((imgrc = OH_ImageReceiverOptions_SetCapacity(hidden->options, imgcapacity)) != IMAGE_SUCCESS) { + return SetImageError("Failed to set image receiver capacity option", imgrc); + } else if ((imgrc = OH_ImageReceiverNative_Create(hidden->options, &hidden->image_receiver)) != IMAGE_SUCCESS) { + return SetImageError("Failed to create native image receiver", imgrc); + } else if ((imgrc = OH_ImageReceiverNative_On(hidden->image_receiver, OnImageReceiverFrame)) != IMAGE_SUCCESS) { + return SetImageError("Failed to set native image receiver callback", imgrc); + } else if ((imgrc = OH_ImageReceiverNative_GetReceivingSurfaceId(hidden->image_receiver, &receiver_surface_id)) != IMAGE_SUCCESS) { + return SetImageError("Failed to create native image receiver", imgrc); + } else if (SDL_snprintf(receiver_surface_id_str, sizeof (receiver_surface_id_str), "%" SDL_PRIu64, receiver_surface_id) >= sizeof (receiver_surface_id_str)) { + return SDL_OutOfMemory(); // shouldn't happen. + } else if ((rc = OH_CameraDevice_GetCameraOrientation(ohdevice, &orientation)) != CAMERA_OK) { + return SetCameraError("Failed to determine camera orientation", rc); + } else if ((rc = OH_CameraManager_CreatePreviewOutput(camera_manager, &video_profile, receiver_surface_id_str, &hidden->output)) != CAMERA_OK) { + //} else if ((rc = OH_CameraManager_CreateVideoOutput(camera_manager, &video_profile, receiver_surface_id_str, &hidden->output)) != CAMERA_OK) { + return SetCameraError("Failed to create video output", rc); + } else if ((rc = OH_PreviewOutput_RegisterCallback(hidden->output, &video_output_callbacks)) != CAMERA_OK) { + //} else if ((rc = OH_VideoOutput_RegisterCallback(hidden->output, &video_output_callbacks)) != CAMERA_OK) { + return SetCameraError("Failed to set video output callbacks", rc); + } else if ((rc = OH_CameraManager_CreateCameraInput(camera_manager, ohdevice, &hidden->input)) != CAMERA_OK) { + return SetCameraError("Failed to create camera input", rc); + } else if ((rc = OH_CameraInput_RegisterCallback(hidden->input, &camera_input_callbacks)) != CAMERA_OK) { + return SetCameraError("Failed to register camera input callbacks", rc); + } else if ((rc = OH_CameraInput_Open(hidden->input)) != CAMERA_OK) { + return SetCameraError("Failed to open camera input", rc); + } else if ((rc = OH_CameraManager_CreateCaptureSession(camera_manager, &hidden->session)) != CAMERA_OK) { + return SetCameraError("Failed to create camera capture session", rc); + } else if ((rc = OH_CaptureSession_SetSessionMode(hidden->session, NORMAL_VIDEO)) != CAMERA_OK) { + return SetCameraError("Failed to set camera capture session mode", rc); + } else if ((rc = OH_CaptureSession_BeginConfig(hidden->session)) != CAMERA_OK) { + return SetCameraError("Failed to begin camera capture session config", rc); + // !!! FIXME: maybe? } else if ((rc = OH_CaptureSession_SetActiveColorSpace(hidden->session, colorspace)) != CAMERA_OK) { + // !!! FIXME: maybe? return SetCameraError("Failed to set active colorspace in session config", rc); + } else if ((rc = OH_CaptureSession_AddInput(hidden->session, hidden->input)) != CAMERA_OK) { + return SetCameraError("Failed to add device input to camera capture session", rc); + } else if ((rc = OH_CaptureSession_AddPreviewOutput(hidden->session, hidden->output)) != CAMERA_OK) { + //} else if ((rc = OH_CaptureSession_AddVideoOutput(hidden->session, hidden->output)) != CAMERA_OK) { + return SetCameraError("Failed to add video output to camera capture session", rc); + } else if ((rc = OH_CaptureSession_CommitConfig(hidden->session)) != CAMERA_OK) { + return SetCameraError("Failed to commit camera capture session config", rc); + } else if ((rc = OH_CaptureSession_Start(hidden->session)) != CAMERA_OK) { + return SetCameraError("Failed to start camera capture session", rc); + //} else if ((rc = OH_VideoOutput_Start(hidden->output)) != CAMERA_OK) { + // return SetCameraError("Failed to start video output", rc); + } + + // we don't need this, it's only in `hidden` so it'll clean up if we fail halfway through this. + OH_ImageReceiverOptions_Release(hidden->options); + hidden->options = NULL; + + hidden->rotation = (int) (orientation % 360); + + return true; +} + +static void SDLCALL CameraPermissionCallback(void *userdata, const char *permission, bool granted) +{ + SDL_Camera *device = (SDL_Camera *) userdata; + if (device->hidden != NULL) { // if device was already closed, don't send an event. + if (!granted) { + SDL_CameraPermissionOutcome(device, false); // sorry, permission denied. + } else if (!PrepareCamera(device)) { // permission given? Actually open the camera now. + // uhoh, setup failed; since the app thinks we already "opened" the device, mark it as disconnected and don't report the permission. + SDL_CameraDisconnected(device); + } else { + // okay! We have permission to use the camera _and_ opening the hardware worked out, report that the camera is usable! + SDL_CameraPermissionOutcome(device, true); // go go go! + } + } + + UnrefPhysicalCamera(device); // we ref'd this in OpenDevice, release the extra reference. +} + + +static bool OPENHARMONYCAMERA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec) +{ +#if 0 + // !!! FIXME: Android requires all concurrent cameras to be opened before assigning them all to a session. + // !!! FIXME: OpenHarmony, in API level 18, added OH_CameraInput_OpenConcurrentCameras() and some other support APIs, + // !!! FIXME: which is probably the way to handle this, but we're adopting SDL's current Android attitude for now: if it fails, okay. + if (CheckDevicePlaying()) { + return SDL_SetError("A camera is already playing"); + } +#endif + + if (spec->framerate_denominator != 1) { + return SDL_SetError("Invalid framerate"); // we force these to fps/1 + } + + device->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData)); + if (device->hidden == NULL) { + return false; + } + + RefPhysicalCamera(device); // ref'd until permission callback fires. + + // just in case SDL_OpenCamera is overwriting device->spec as CameraPermissionCallback runs, we work from a different copy. + SDL_copyp(&device->hidden->requested_spec, spec); + if (!SDL_RequestOpenHarmonyPermission("ohos.permission.CAMERA", CameraPermissionCallback, device)) { + UnrefPhysicalCamera(device); + return false; + } + + return true; // we don't open the camera until permission is granted, so always succeed for now. +} + +static void OPENHARMONYCAMERA_FreeDeviceHandle(SDL_Camera *device) +{ + if (device) { + Camera_Device *ohdevice = (Camera_Device *) device->handle; + SDL_free(ohdevice->cameraId); + SDL_free(ohdevice); + } +} + +static void GatherCameraSpecs(Camera_Device *ohdevice, CameraFormatAddData *add_data, char **fullname, SDL_CameraPosition *position) +{ + Camera_ErrorCode rc = CAMERA_OK; + SDL_zerop(add_data); + + *fullname = NULL; // !!! FIXME: there doesn't appear to be a "get device's name" API for obtaining something like "Logitech WebCam". + + if (ohdevice->cameraPosition == CAMERA_POSITION_FRONT) { + *position = SDL_CAMERA_POSITION_FRONT_FACING; + if (!*fullname) { + *fullname = SDL_strdup("Front-facing camera"); + } + } else if (ohdevice->cameraPosition == CAMERA_POSITION_BACK) { + *position = SDL_CAMERA_POSITION_BACK_FACING; + if (!*fullname) { + *fullname = SDL_strdup("Back-facing camera"); + } + } else { + *position = SDL_CAMERA_POSITION_UNKNOWN; + } + + if (!*fullname) { + *fullname = SDL_strdup("Generic camera"); // we tried. + } + + Camera_OutputCapability *output_caps = NULL; + rc = OH_CameraManager_GetSupportedCameraOutputCapabilityWithSceneMode(camera_manager, ohdevice, NORMAL_VIDEO, &output_caps); + if ((rc != CAMERA_OK) || !output_caps) { + return; + } + + const uint32_t total = output_caps->previewProfilesSize; + //const uint32_t total = output_caps->videoProfilesSize; + for (uint32_t i = 0; i < total; i++) { + const Camera_Profile *vidprof = output_caps->previewProfiles[i]; + //const Camera_VideoProfile *vidprof = output_caps->videoProfiles[i]; + SDL_PixelFormat device_format = SDL_PIXELFORMAT_UNKNOWN; + SDL_Colorspace device_colorspace = SDL_COLORSPACE_UNKNOWN; + const int w = (int) vidprof->size.width; + const int h = (int) vidprof->size.height; + if ((w <= 0) || (h <= 0)) { + continue; + } else { + ConvertOpenHarmonyFormatToSDL(vidprof->format, &device_format, &device_colorspace); + if (device_format == SDL_PIXELFORMAT_UNKNOWN) { + continue; + } + } + + SDL_AddCameraFormat(add_data, device_format, device_colorspace, w, h, 60, 1); + //SDL_AddCameraFormat(add_data, device_format, device_colorspace, w, h, vidprof->range.min, 1); + //if (vidprof->range.min != vidprof->range.max) { + // SDL_AddCameraFormat(add_data, device_format, device_colorspace, w, h, vidprof->range.max, 1); + //} + } + + OH_CameraManager_DeleteSupportedCameraOutputCapability(camera_manager, output_caps); +} + +static void MaybeAddDevice(Camera_Device *ohdevice) +{ + const char *devid = ohdevice->cameraId; + Camera_ErrorCode rc; + + #if DEBUG_CAMERA + SDL_Log("CAMERA: MaybeAddDevice('%s')", devid); + #endif + + if (SDL_FindPhysicalCameraByCallback(FindOpenHarmonyCameraByDevicePtr, ohdevice)) { + return; // already have this one. + } + + Camera_SceneMode *scene_modes = NULL; + uint32_t num_scene_modes = 0; + rc = OH_CameraManager_GetSupportedSceneModes(ohdevice, &scene_modes, &num_scene_modes); + if ((rc != CAMERA_OK) || !scene_modes) { + return; + } + + bool okay = false; + for (uint32_t i = 0; i < num_scene_modes; i++) { + if (scene_modes[i] == NORMAL_VIDEO) { + okay = true; + break; + } + } + + OH_CameraManager_DeleteSceneModes(camera_manager, scene_modes); + + if (!okay) { + return; // uh...weird camera...? + } + + SDL_CameraPosition position = SDL_CAMERA_POSITION_UNKNOWN; + char *fullname = NULL; + CameraFormatAddData add_data; + GatherCameraSpecs(ohdevice, &add_data, &fullname, &position); + if (add_data.num_specs > 0) { + SDL_Camera *device = NULL; + Camera_Device *devcpy = (Camera_Device *) SDL_malloc(sizeof (*devcpy)); + if (devcpy) { + devcpy->cameraId = SDL_strdup(devid); + if (devcpy->cameraId) { + device = SDL_AddCamera(fullname, position, add_data.num_specs, add_data.specs, devcpy); + } + } + + if (!device) { + if (devcpy) { + SDL_free(devcpy->cameraId); + SDL_free(devcpy); + } + } + } + + SDL_free(fullname); + SDL_free(add_data.specs); +} + +// note that camera "availability" covers both hotplugging and whether another +// has the device opened, but for something like OpenHarmony, it's probably fine +// to treat both unplugging and loss of access as disconnection events. When +// the other app closes the camera, we get an available event as if it was +// just plugged back in. + +static void OnCameraDeviceStatus(Camera_Manager *cameraManager, Camera_StatusInfo *status) +{ + #if DEBUG_CAMERA + SDL_Log("CAMERA: CB OnCameraDeviceStatus('%s', %d)", status->camera->cameraId, (int) status->status); + #endif + SDL_assert(status != NULL); + SDL_assert(status->camera != NULL); + SDL_assert(status->camera->cameraId != NULL); + if (status->status == CAMERA_STATUS_AVAILABLE) { + MaybeAddDevice(status->camera); + } else if (status->status == CAMERA_STATUS_UNAVAILABLE) { + // !!! FIXME: this comment is from Android, is this true here, too? + // THIS CALLBACK FIRES WHEN YOU OPEN THE DEVICE YOURSELF. :( + // Make sure we don't have the device opened, in which case a different callback will fire instead if actually lost. + SDL_Camera *device = SDL_FindPhysicalCameraByCallback(FindOpenHarmonyCameraByDevicePtr, status->camera); + if (device && !device->hidden) { + SDL_CameraDisconnected(device); + } + } +} + + +static CameraManager_Callbacks camera_availability_listener = { OnCameraDeviceStatus }; + +static void OPENHARMONYCAMERA_DetectDevices(void) +{ + Camera_ErrorCode rc = CAMERA_OK; + Camera_Device *cameras = NULL; + uint32_t total = 0; + rc = OH_CameraManager_GetSupportedCameras(camera_manager, &cameras, &total); + + if ((rc == CAMERA_OK) && (total > 0)) { // if this fails, maybe the callback catches things later. + for (uint32_t i = 0; i < total; i++) { + MaybeAddDevice(&cameras[i]); + } + OH_CameraManager_DeleteSupportedCameras(camera_manager, cameras, total); + } + + OH_CameraManager_RegisterCallback(camera_manager, &camera_availability_listener); +} + +static void OPENHARMONYCAMERA_Deinitialize(void) +{ + OH_CameraManager_UnregisterCallback(camera_manager, &camera_availability_listener); + DestroyCameraManager(); +} + +static bool OPENHARMONYCAMERA_Init(SDL_CameraDriverImpl *impl) +{ + if (!CreateCameraManager()) { + return false; + } + + impl->DetectDevices = OPENHARMONYCAMERA_DetectDevices; + impl->OpenDevice = OPENHARMONYCAMERA_OpenDevice; + impl->CloseDevice = OPENHARMONYCAMERA_CloseDevice; + impl->WaitDevice = OPENHARMONYCAMERA_WaitDevice; + impl->AcquireFrame = OPENHARMONYCAMERA_AcquireFrame; + impl->ReleaseFrame = OPENHARMONYCAMERA_ReleaseFrame; + impl->FreeDeviceHandle = OPENHARMONYCAMERA_FreeDeviceHandle; + impl->Deinitialize = OPENHARMONYCAMERA_Deinitialize; + + impl->ProvidesOwnCallbackThread = true; + + return true; +} + +CameraBootStrap OPENHARMONYCAMERA_bootstrap = { + "ohcamera", "SDL OpenHarmony camera driver", OPENHARMONYCAMERA_Init, false +}; + +#endif diff --git a/src/core/SDL_core_unsupported.c b/src/core/SDL_core_unsupported.c index 7f9dbbe0dc..6a8fc6f309 100644 --- a/src/core/SDL_core_unsupported.c +++ b/src/core/SDL_core_unsupported.c @@ -204,3 +204,25 @@ bool SDL_IsUbuntuTouch(void) } #endif + +#ifndef SDL_PLATFORM_OPENHARMONY +const char *SDL_GetOpenHarmonyInternalStoragePath(void) +{ + SDL_Unsupported(); + return NULL; +} + +int SDL_GetOpenHarmonySDKVersion(void) +{ + return SDL_Unsupported(); +} + +bool SDL_RequestOpenHarmonyPermission(const char *permission, SDL_RequestOpenHarmonyPermissionCallback cb, void *userdata) +{ + (void)permission; + (void)cb; + (void)userdata; + return SDL_Unsupported(); +} +#endif + diff --git a/src/core/SDL_core_unsupported.h b/src/core/SDL_core_unsupported.h index d100836019..730503a5f0 100644 --- a/src/core/SDL_core_unsupported.h +++ b/src/core/SDL_core_unsupported.h @@ -69,3 +69,11 @@ extern SDL_DECLSPEC Sint32 SDLCALL JNI_OnLoad(JavaVM *vm, void *reserved); #if !defined(SDL_PLATFORM_LINUX) extern SDL_DECLSPEC bool SDLCALL SDL_IsUbuntuTouch(void); #endif /* !SDL_PLATFORM_LINUX */ + +#if !defined(SDL_PLATFORM_OPENHARMONY) +typedef void *SDL_RequestOpenHarmonyPermissionCallback; +extern SDL_DECLSPEC const char * SDLCALL SDL_GetOpenHarmonyInternalStoragePath(void); +extern SDL_DECLSPEC int SDLCALL SDL_GetOpenHarmonySDKVersion(void); +extern SDL_DECLSPEC bool SDLCALL SDL_RequestOpenHarmonyPermission(const char *permission, SDL_RequestOpenHarmonyPermissionCallback cb, void *userdata); +#endif + diff --git a/src/core/android/SDL_android.h b/src/core/android/SDL_android.h index 01074612e9..bb5f06eac1 100644 --- a/src/core/android/SDL_android.h +++ b/src/core/android/SDL_android.h @@ -113,6 +113,10 @@ bool Android_JNI_FileClose(void *userdata); bool Android_JNI_EnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata); bool Android_JNI_GetAssetPathInfo(const char *path, SDL_PathInfo *info); +#define SDL_PlatformEnumerateAssetDirectory Android_JNI_EnumerateAssetDirectory +#define SDL_GetPlatformInternalStoragePath SDL_GetAndroidInternalStoragePath +#define SDL_PlatformGetAssetPathInfo Android_JNI_GetAssetPathInfo + // Environment support void Android_JNI_GetManifestEnvironmentVariables(void); int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode); diff --git a/src/core/openharmony/SDL_openharmony.c b/src/core/openharmony/SDL_openharmony.c new file mode 100644 index 0000000000..a8b6872c45 --- /dev/null +++ b/src/core/openharmony/SDL_openharmony.c @@ -0,0 +1,1207 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_PLATFORM_OPENHARMONY + +#include "../../events/SDL_events_c.h" + +// !!! FIXME: work around some C++isms that leaked into OpenHarmony headers. +// !!! FIXME: HACK to prevent from including. It has '&' instead of '*' for some args, which suggests it's only been tested with C++. +#define ABILITY_RUNTIME_START_OPTIONS_H +typedef enum AbilityRuntime_StartOptions AbilityRuntime_StartOptions; + + +// !!! FIXME: has functions with C++ references. Prevent raw_file_manager.h from including it, and define the parts we need here. +#define GLOBAL_RAW_FILE_H +typedef struct RawFile RawFile; +typedef struct RawFile64 RawFile64; +typedef struct { int fd; long start; long length; } RawFileDescriptor; +typedef struct { int fd; int64_t start; int64_t length; } RawFileDescriptor64; +int64_t OH_ResourceManager_GetRawFileSize64(RawFile64 *rawFile) __attribute__((__availability__(ohos, introduced=11.0.0))); +int OH_ResourceManager_SeekRawFile64(const RawFile64 *rawFile, int64_t offset, int whence) __attribute__((__availability__(ohos, introduced=11.0.0))); +int64_t OH_ResourceManager_ReadRawFile64(const RawFile64 *rawFile, void *buf, int64_t length) __attribute__((__availability__(ohos, introduced=11.0.0))); +int64_t OH_ResourceManager_GetRawFileRemainingLength64(const RawFile64 *rawFile) __attribute__((__availability__(ohos, introduced=11.0.0))); +int64_t OH_ResourceManager_GetRawFileOffset64(const RawFile64 *rawFile) __attribute__((__availability__(ohos, introduced=11.0.0))); +void OH_ResourceManager_CloseRawFile64(RawFile64 *rawFile) __attribute__((__availability__(ohos, introduced=11.0.0))); +bool OH_ResourceManager_GetRawFileDescriptor64(const RawFile64 *rawFile, RawFileDescriptor64 *descriptor) __attribute__((__availability__(ohos, introduced=11.0.0))); +bool OH_ResourceManager_ReleaseRawFileDescriptor64(const RawFileDescriptor64 *descriptor) __attribute__((__availability__(ohos, introduced=11.0.0))); + +#include + +// !!! FIXME: these are defined as "const uint32_t VARNAME = VALUE;" in native_interface_xcomponent.h, which becomes a global variable in _our_ C code! Maybe C++ handles this differently...? +#define OH_XCOMPONENT_ID_LEN_MAX sdl_core_ohos_OH_XCOMPONENT_ID_LEN_MAX +#define OH_MAX_TOUCH_POINTS_NUMBER sdl_core_ohos_OH_MAX_TOUCH_POINTS_NUMBER +#include + +#include +#include +#include +#include +#include +#include +#include + +// !!! FIXME: which of these headers do we actually need? +#include +#include +#include +#include +#include + +#include "SDL_openharmony.h" +#include "../../video/openharmony/SDL_openharmonyvideo.h" +#include "../../video/openharmony/SDL_openharmonyevents.h" + +static CommonEvent_Subscriber *commonevent_subscriber = NULL; +static napi_ref ability_object_ref = NULL; +static napi_ref atmanager_ref = NULL; +static napi_ref window_ref = NULL; +static napi_ref ime_controller_ref = NULL; +static napi_ref pointer_ref = NULL; +static napi_ref on_insert_text_ref = NULL; +static napi_ref on_delete_left_ref = NULL; +static napi_ref i18nsystem_ref = NULL; +static NativeResourceManager *native_resource_mgr = NULL; +static napi_threadsafe_function syslocalechanged_threadsafefn = NULL; +static napi_threadsafe_function req_permissions_threadsafefn = NULL; +static napi_threadsafe_function open_url_threadsafefn = NULL; +static napi_threadsafe_function change_sysbars_threadsafefn = NULL; +static napi_threadsafe_function change_screensaver_threadsafefn = NULL; +static napi_threadsafe_function show_screenkeyboard_threadsafefn = NULL; +static napi_threadsafe_function hide_screenkeyboard_threadsafefn = NULL; +static napi_threadsafe_function change_mouseptr_threadsafefn = NULL; +static char *system_locale = NULL; +static SDL_SystemTheme system_theme = SDL_SYSTEM_THEME_UNKNOWN; +static SDL_DisplayOrientation device_orientation = SDL_ORIENTATION_PORTRAIT; + + +// Some NAPI helper code... + +#define SDL_JS_ENTRY(expected_argc) \ + size_t argc = expected_argc; \ + napi_value argv[expected_argc]; \ + napi_value self = NULL; \ + napi_get_cb_info(env, info, &argc, argv, &self, NULL) + +#define SDL_JS_ENTRY_USERDATA(expected_argc, userdata_type) \ + userdata_type *userdata = NULL; \ + size_t argc = expected_argc; \ + napi_value argv[expected_argc]; \ + napi_value self = NULL; \ + napi_get_cb_info(env, info, &argc, argv, &self, (void **) &userdata) + +#define CallNapiThreadsafeFunction napi_call_threadsafe_function +#define SetNapiArrayElement napi_set_element +#define SetNapiObjField napi_set_named_property + +static char *CreateSDLStringFromNAPIValue(napi_env env, napi_value val) +{ + char *retval = NULL; + size_t buflen = 0; + if (napi_get_value_string_utf8(env, val, NULL, 0, &buflen) != napi_ok) { + return NULL; + } else if ((retval = (char *) SDL_malloc(buflen + 1)) == NULL) { + return NULL; + } else if (napi_get_value_string_utf8(env, val, retval, buflen + 1, &buflen) != napi_ok) { + SDL_free(retval); + return NULL; + } + return retval; +} + +static napi_value CreateNapiString(napi_env env, const char *str) +{ + napi_value retval = NULL; + if (napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, &retval) != napi_ok) { + return NULL; + } + return retval; +} + +static napi_value CreateNapiObject(napi_env env) +{ + napi_value retval = NULL; + if (napi_create_object(env, &retval) != napi_ok) { + return NULL; + } + return retval; +} + +static napi_value CreateNapiArray(napi_env env, size_t len) +{ + napi_value retval = NULL; + if (napi_create_array_with_length(env, len, &retval) != napi_ok) { + return NULL; + } + return retval; +} + +static napi_value CreateNapiFunction(napi_env env, const char *name, napi_callback cb, void *userdata) +{ + napi_value retval = NULL; + if (napi_create_function(env, name, name ? NAPI_AUTO_LENGTH : 0, cb, userdata, &retval) != napi_ok) { + return NULL; + } + return retval; +} + +static napi_value GetNapiArrayElement(napi_env env, napi_value arr, int idx) +{ + napi_value retval = NULL; + if (napi_get_element(env, arr, idx, &retval) != napi_ok) { + return NULL; + } + return retval; +} + +static napi_value CreateNapiInt(napi_env env, int val) +{ + napi_value retval = NULL; + if (napi_create_int32(env, (int32_t) val, &retval) != napi_ok) { + return NULL; + } + return retval; +} + +static int GetNapiInt(napi_env env, napi_value val, int deflt) +{ + int32_t retval = (int32_t) deflt; + if (napi_get_value_int32(env, val, &retval) != napi_ok) { + return deflt; + } + return (int) retval; +} + +static napi_value GetNapiBoolean(napi_env env, bool b) +{ + napi_value retval = NULL; + if (napi_get_boolean(env, b, &retval) != napi_ok) { + return NULL; + } + return retval; +} + +static napi_value GetNapiUndefined(napi_env env) +{ + napi_value retval = NULL; + napi_get_undefined(env, &retval); + return retval; +} + +static napi_value GetNapiObjField(napi_env env, napi_value obj, const char *fieldname) +{ + napi_value retval = NULL; + if (napi_get_named_property(env, obj, fieldname, &retval) != napi_ok) { + return NULL; + } + return retval; +} + +static napi_value GetNapiRefValue(napi_env env, napi_ref ref) +{ + napi_value retval = NULL; + if (napi_get_reference_value(env, ref, &retval) != napi_ok) { + return NULL; + } + return retval; +} + +static napi_threadsafe_function CreateNapiThreadsafeFunction(napi_env env, const char *name, napi_threadsafe_function_call_js fn) +{ + napi_threadsafe_function retval = NULL; + if (napi_create_threadsafe_function(env, NULL, NULL, CreateNapiString(env, name), 0, 1, NULL, NULL, NULL, fn, &retval) != napi_ok) { + return NULL; + } + return retval; +} + +static napi_value CallNapiMethod(napi_env env, napi_value obj, const char *method, int num_args, napi_value *args) +{ + napi_value retval = NULL; + if (napi_call_function(env, obj, GetNapiObjField(env, obj, method), num_args, args, &retval) != napi_ok) { + return NULL; + } + return retval; +} + + +int SDL_GetOpenHarmonySDKVersion(void) +{ + static int sdk_version; + if (!sdk_version) { + sdk_version = OH_GetSdkApiVersion(); + } + return sdk_version; +} + +SDL_FormFactor SDL_GetOpenHarmonyDeviceFormFactor(void) +{ + static bool checked = false; + static SDL_FormFactor form_factor = SDL_FORMFACTOR_UNKNOWN; + if (!checked) { + checked = true; + const char *devtype = OH_GetDeviceType(); + if (devtype) { + if ((SDL_strcmp(devtype, "phone") == 0) || (SDL_strcmp(devtype, "default") == 0)) { + form_factor = SDL_FORMFACTOR_PHONE; + } else if (SDL_strcmp(devtype, "wearable") == 0) { // !!! FIXME: what is the difference between a "wearable" and "liteWearable"? + form_factor = SDL_FORMFACTOR_WATCH; + } else if (SDL_strcmp(devtype, "liteWearable") == 0) { + form_factor = SDL_FORMFACTOR_WATCH; + } else if (SDL_strcmp(devtype, "tablet") == 0) { + form_factor = SDL_FORMFACTOR_TABLET; + } else if (SDL_strcmp(devtype, "tv") == 0) { + form_factor = SDL_FORMFACTOR_TV; + } else if (SDL_strcmp(devtype, "car") == 0) { + form_factor = SDL_FORMFACTOR_CAR; + } else if (SDL_strcmp(devtype, "smartVision") == 0) { // !!! FIXME: I assume this is either some sort of AI glasses or a VR headset...? + form_factor = SDL_FORMFACTOR_HEADSET; + } else { + // !!! FIXME: aren't there desktops that can run HarmonyOS? Or are there just tablets? + } + } + } + return form_factor; +} + +void SDL_DebugLogOpenHarmonyInfo(void) +{ + static bool already_logged = false; + if (!already_logged) { + already_logged = true; + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "SDL OpenHarmony system/device info:"); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Device type: %s", OH_GetDeviceType()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Manufacturer: %s", OH_GetManufacture()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Brand: %s", OH_GetBrand()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Market name: %s", OH_GetMarketName()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Product series: %s", OH_GetProductSeries()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Product model: %s", OH_GetProductModel()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Software model: %s", OH_GetSoftwareModel()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Hardware model: %s", OH_GetHardwareModel()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Bootloader version: %s", OH_GetBootloaderVersion()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - ABI list: %s", OH_GetAbiList()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Security patch tag: %s", OH_GetSecurityPatchTag()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Display version: %s", OH_GetDisplayVersion()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Incremental version: %s", OH_GetIncrementalVersion()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - OS release type: %s", OH_GetOsReleaseType()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - OS full name: %s", OH_GetOSFullName()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - SDK API version: %d", SDL_GetOpenHarmonySDKVersion()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - First API version: %d", OH_GetFirstApiVersion()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Version ID: %s", OH_GetVersionId()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Build type: %s", OH_GetBuildType()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Build user: %s", OH_GetBuildUser()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Build host: %s", OH_GetBuildHost()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Build time: %s", OH_GetBuildTime()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Build root hash: %s", OH_GetBuildRootHash()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Distribution OS name: %s", OH_GetDistributionOSName()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Distribution OS version: %s", OH_GetDistributionOSVersion()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Distribution OS API version: %d", OH_GetDistributionOSApiVersion()); + SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Distribution OS release type: %s", OH_GetDistributionOSReleaseType()); + } +} + +const char *SDL_GetOpenHarmonySystemLocale(void) +{ + return system_locale; +} + +SDL_SystemTheme SDL_GetOpenHarmonySystemTheme(void) +{ + return system_theme; +} + +SDL_DisplayOrientation SDL_GetOpenHarmonyDeviceCurrentOrientation(void) +{ + return device_orientation; +} + +const char *SDL_GetOpenHarmonyInternalStoragePath(void) +{ + static char *files_path = NULL; + if (!files_path) { + char *path = NULL; + int32_t writelen = 0; + size_t slen = 128; + while (true) { + void *ptr = SDL_realloc(path, slen + 1); // +1 to save space for null terminator. + if (!ptr) { + SDL_free(path); + return NULL; + } + path = (char *) ptr; + + const AbilityRuntime_ErrorCode rc = OH_AbilityRuntime_ApplicationContextGetFilesDir(path, slen, &writelen); + if (rc == ABILITY_RUNTIME_ERROR_CODE_NO_ERROR) { + break; + } else if (rc != ABILITY_RUNTIME_ERROR_CODE_PARAM_INVALID) { + SDL_SetError("OH_AbilityRuntime_ApplicationContextGetFilesDir failed: %d", (int) rc); + SDL_free(path); + return NULL; + } + slen *= 2; // try again with a bigger buffer. + } + + files_path = (char *) SDL_realloc(path, SDL_strlen(path) + 1); // shrink it down. + if (!files_path) { + files_path = path; // oh well, _don't_ shrink it down... + } + } + + return files_path; +} + + +// Filesystem stuff... + +bool SDL_OpenHarmonyRawFileOpen(void **puserdata, const char *fileName, const char *mode) +{ + if (SDL_strncmp(fileName, "assets://", 9) == 0) { + fileName += 9; + } + + SDL_assert(native_resource_mgr != NULL); // ArkTS should have sent us this at startup. + SDL_assert(puserdata != NULL); + + if (mode && (SDL_strcmp(mode, "r") != 0) && (SDL_strcmp(mode, "rb") != 0)) { + return SDL_SetError("RawFile access is read-only"); + } + + RawFile64 *rf64 = OH_ResourceManager_OpenRawFile64(native_resource_mgr, fileName); + if (!rf64) { + return SDL_SetError("Failed to open RawFile"); + } + + *puserdata = rf64; + return true; +} + +Sint64 SDL_OpenHarmonyRawFileSize(void *userdata) +{ + return (Sint64) OH_ResourceManager_GetRawFileSize64((RawFile64 *) userdata); +} + +Sint64 SDL_OpenHarmonyRawFileSeek(void *userdata, Sint64 offset, SDL_IOWhence whence) +{ + const int ohwhence = (int) whence; // these values happen to match. + if (OH_ResourceManager_SeekRawFile64((const RawFile64 *) userdata, (int64_t) offset, ohwhence) < 0) { + SDL_SetError("RawFile seek failed"); + return -1; + } + return (Sint64) OH_ResourceManager_GetRawFileOffset64((const RawFile64 *) userdata); +} + +size_t SDL_OpenHarmonyRawFileRead(void *userdata, void *buffer, size_t size, SDL_IOStatus *status) +{ + const size_t br = (size_t) OH_ResourceManager_ReadRawFile64((const RawFile64 *) userdata, buffer, (int64_t) size); // this returns 0 on eof/error, not a negative, so just cast to size_t. + if (br < size) { + if (OH_ResourceManager_GetRawFileRemainingLength64((const RawFile64 *) userdata) == 0) { + *status = SDL_IO_STATUS_EOF; + } else { + *status = SDL_IO_STATUS_ERROR; + SDL_SetError("RawFile read failed"); + } + } + return br; +} + +bool SDL_OpenHarmonyRawFileClose(void *userdata) +{ + OH_ResourceManager_CloseRawFile64((RawFile64 *) userdata); + return true; +} + +bool SDL_OpenHarmonyEnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata) +{ + const char *origpath = path; + if (SDL_strncmp(path, "assets://", 9) == 0) { + path += 9; + } + + SDL_assert(native_resource_mgr != NULL); // ArkTS should have sent us this at startup. + RawDir *rawdir = OH_ResourceManager_OpenRawDir(native_resource_mgr, path); + if (!rawdir) { + return SDL_SetError("RawDir open failed"); + } + + SDL_EnumerationResult result = SDL_ENUM_CONTINUE; + const int total = OH_ResourceManager_GetRawFileCount(rawdir); + for (int i = 0; (i < total) && (result == SDL_ENUM_CONTINUE); i++) { + const char *fname = OH_ResourceManager_GetRawFileName(rawdir, i); + result = cb(userdata, origpath, fname); + } + + OH_ResourceManager_CloseRawDir(rawdir); + + return (result != SDL_ENUM_FAILURE); +} + +bool SDL_OpenHarmonyGetAssetPathInfo(const char *path, SDL_PathInfo *info) +{ + if (SDL_strncmp(path, "assets://", 9) == 0) { + path += 9; + } + + SDL_assert(native_resource_mgr != NULL); // ArkTS should have sent us this at startup. + SDL_zerop(info); + if (OH_ResourceManager_IsRawDir(native_resource_mgr, path)) { + info->type = SDL_PATHTYPE_DIRECTORY; + } else { + RawFile64 *rf64 = OH_ResourceManager_OpenRawFile64(native_resource_mgr, path); + if (!rf64) { + return SDL_SetError("No such file or directory"); + } + info->type = SDL_PATHTYPE_FILE; + info->size = (Uint64) OH_ResourceManager_GetRawFileSize64(rf64); + OH_ResourceManager_CloseRawFile64(rf64); + } + return true; +} + + +// ArkTS/C bridging... + +typedef struct RequestPermissionData +{ + char *permission; + SDL_RequestOpenHarmonyPermissionCallback callback; + void *callback_userdata; +} RequestPermissionData; + +// AsyncCallback when CallJSRequestPermissions() finishes its work. +static napi_value SDL_JS_RequestPermissionResult(napi_env env, napi_callback_info info) +{ + SDL_JS_ENTRY_USERDATA(2, RequestPermissionData); + napi_value result = GetNapiArrayElement(env, GetNapiObjField(env, argv[1], "authResults"), 0); + const bool granted = (GetNapiInt(env, result, -1) == 0); + userdata->callback(userdata->callback_userdata, userdata->permission, granted); + + SDL_free(userdata->permission); + SDL_free(userdata); + + return GetNapiUndefined(env); +} + +// this function is called from the main Javascript thread when it's convenient to fire it. +static void CallJSRequestPermissions(napi_env env, napi_value js_callback, void *context, void *userdata) +{ + RequestPermissionData *data = (RequestPermissionData *) userdata; + napi_value ability = GetNapiRefValue(env, ability_object_ref); + napi_value atmanager = GetNapiRefValue(env, atmanager_ref); + napi_value args[] = { GetNapiObjField(env, ability, "context"), CreateNapiArray(env, 1), CreateNapiFunction(env, NULL, SDL_JS_RequestPermissionResult, data) }; + SetNapiArrayElement(env, args[1], 0, CreateNapiString(env, data->permission)); + CallNapiMethod(env, atmanager, "requestPermissionsFromUser", SDL_arraysize(args), args); + // okay, assuming this worked out, we'll get a callback to SDL_JS_RequestPermissionResult() at some point in the future (if we haven't already). +} + +bool SDL_RequestOpenHarmonyPermission(const char *permission, SDL_RequestOpenHarmonyPermissionCallback cb, void *userdata) +{ + RequestPermissionData *data = NULL; + + if (!permission) { + return SDL_InvalidParamError("permission"); + } else if (!cb) { + return SDL_InvalidParamError("cb"); + } else if (!atmanager_ref) { + return SDL_SetError("atManager not initialized"); + } else if ((data = (RequestPermissionData *) SDL_calloc(1, sizeof (*data))) == NULL) { + return false; + } else if ((data->permission = SDL_strdup(permission)) == NULL) { + SDL_free(data); + return false; + } + + data->callback = cb; + data->callback_userdata = userdata; + + return (CallNapiThreadsafeFunction(req_permissions_threadsafefn, data, napi_tsfn_nonblocking) == napi_ok); +} + +// AsyncCallback when CallJSOpenURL() finishes its work. +static napi_value SDL_JS_OpenURLResult(napi_env env, napi_callback_info info) +{ + SDL_JS_ENTRY_USERDATA(1, char); + SDL_free(userdata); + return GetNapiUndefined(env); +} + +// this function is called from the main Javascript thread when it's convenient to fire it. +static void CallJSOpenURL(napi_env env, napi_value js_callback, void *context, void *userdata) +{ + char *url = (char *) userdata; + napi_value ability = GetNapiRefValue(env, ability_object_ref); + napi_value abcontext = GetNapiObjField(env, ability, "context"); + napi_value args[] = { CreateNapiString(env, url), CreateNapiFunction(env, NULL, SDL_JS_OpenURLResult, url) }; + CallNapiMethod(env, abcontext, "openLink", SDL_arraysize(args), args); +} + +bool SDL_OpenHarmonyOpenURL(const char *url) +{ + char *data = NULL; + if (!url) { + return SDL_InvalidParamError("url"); + } else if (!ability_object_ref) { + return SDL_SetError("Ability not initialized"); + } else if ((data = SDL_strdup(url)) == NULL) { + return false; + } + return (CallNapiThreadsafeFunction(open_url_threadsafefn, data, napi_tsfn_nonblocking) == napi_ok); +} + + +// AsyncCallback when CallJSChangeSysBars() finishes its work. +static napi_value SDL_JS_ChangeSysBarsResult(napi_env env, napi_callback_info info) +{ + return GetNapiUndefined(env); +} + +// this function is called from the main Javascript thread when it's convenient to fire it. +static void CallJSChangeSysBars(napi_env env, napi_value js_callback, void *context, void *userdata) +{ + const intptr_t flags = (const intptr_t) userdata; + const bool status_bar = (flags & (1 << 0)) != 0; + const bool navigation_bar = (flags & (1 << 1)) != 0; + int arrlen = 0; + if (status_bar) { arrlen++; } + if (navigation_bar) { arrlen++; } + + napi_value arr = CreateNapiArray(env, arrlen); + arrlen = 0; + if (status_bar) { + SetNapiArrayElement(env, arr, arrlen++, CreateNapiString(env, "status")); + } + if (navigation_bar) { + SetNapiArrayElement(env, arr, arrlen++, CreateNapiString(env, "navigation")); + } + + napi_value window = GetNapiRefValue(env, window_ref); + napi_value args[] = { arr, CreateNapiFunction(env, NULL, SDL_JS_ChangeSysBarsResult, NULL) }; + CallNapiMethod(env, window, "setSystemBarEnable", SDL_arraysize(args), args); +} + +bool SDL_OpenHarmonyToggleSystemBars(bool status_bar, bool navigation_bar) +{ + const intptr_t flags = (status_bar ? (1 << 0) : 0) | (navigation_bar ? (1 << 1) : 0); + if (!window_ref) { + return SDL_SetError("Window not initialized"); + } + return (CallNapiThreadsafeFunction(change_sysbars_threadsafefn, (void *) flags, napi_tsfn_nonblocking) == napi_ok); +} + +// AsyncCallback when CallJSChangeScreenSaver() finishes its work. +static napi_value SDL_JS_ChangeScreenSaverResult(napi_env env, napi_callback_info info) +{ + return GetNapiUndefined(env); +} + +// this function is called from the main Javascript thread when it's convenient to fire it. +static void CallJSChangeScreenSaver(napi_env env, napi_value js_callback, void *context, void *userdata) +{ + napi_value window = GetNapiRefValue(env, window_ref); + napi_value args[] = { GetNapiBoolean(env, (userdata != NULL)), CreateNapiFunction(env, NULL, SDL_JS_ChangeScreenSaverResult, NULL) }; + CallNapiMethod(env, window, "setWindowKeepScreenOn", SDL_arraysize(args), args); +} + +bool SDL_OpenHarmonyChangeScreenSaver(bool enable) +{ + if (!window_ref) { + return SDL_SetError("Window not initialized"); + } + return (CallNapiThreadsafeFunction(change_screensaver_threadsafefn, (void *) (size_t) (enable ? 0x1 : 0x0), napi_tsfn_nonblocking) == napi_ok); +} + +// Called by the IME controller for insertText events. +static napi_value SDL_JS_IME_Controller_OnInsertText(napi_env env, napi_callback_info info) +{ + SDL_JS_ENTRY(1); + napi_value text = argv[0]; + char *utf8 = CreateSDLStringFromNAPIValue(env, text); + if (utf8) { + SDL_SendKeyboardText(utf8); + SDL_free(utf8); + } + return GetNapiUndefined(env); +} + +// Called by the IME controller for deleteLeft events. +static napi_value SDL_JS_IME_Controller_OnDeleteLeft(napi_env env, napi_callback_info info) +{ + SDL_JS_ENTRY(1); + napi_value number = argv[0]; + const int total = GetNapiInt(env, number, 0); + for (int i = 0; i < total; i++) { + SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, true); + SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, false); + } + return GetNapiUndefined(env); +} + + +typedef struct ShowKeyboardData +{ + int input_type; + int cap_type; + SDL_Rect *text_input_rect; + SDL_Rect text_input_rect_data; +} ShowKeyboardData; + +// AsyncCallback when CallJSShowScreenKeyboard() finishes its work. +static napi_value SDL_JS_ShowScreenKeyboardResult(napi_env env, napi_callback_info info) +{ + SDL_JS_ENTRY_USERDATA(0, ShowKeyboardData); + napi_value ime_controller = GetNapiRefValue(env, ime_controller_ref); + napi_value ime_oninserttext_args[] = { CreateNapiString(env, "insertText"), GetNapiRefValue(env, on_insert_text_ref) }; + CallNapiMethod(env, ime_controller, "on", SDL_arraysize(ime_oninserttext_args), ime_oninserttext_args); + napi_value ime_ondeleteleft_args[] = { CreateNapiString(env, "deleteLeft"), GetNapiRefValue(env, on_delete_left_ref) }; + CallNapiMethod(env, ime_controller, "on", SDL_arraysize(ime_ondeleteleft_args), ime_ondeleteleft_args); + SDL_SendScreenKeyboardShown(); + SDL_free(userdata); + return GetNapiUndefined(env); +} + +// this function is called from the main Javascript thread when it's convenient to fire it. +static void CallJSShowScreenKeyboard(napi_env env, napi_value js_callback, void *context, void *userdata) +{ + ShowKeyboardData *data = (ShowKeyboardData *) userdata; + napi_value ime_controller = GetNapiRefValue(env, ime_controller_ref); + + napi_value inputAttribute = CreateNapiObject(env); + SetNapiObjField(env, inputAttribute, "textInputType", CreateNapiInt(env, data->input_type)); + SetNapiObjField(env, inputAttribute, "capitalizeMode", CreateNapiInt(env, data->cap_type)); + SetNapiObjField(env, inputAttribute, "enterKeyType", CreateNapiInt(env, 0 /*inputMethod.enterKeyType.UNSPECIFIED*/)); // !!! FIXME + + napi_value attach_params = CreateNapiObject(env); + SetNapiObjField(env, attach_params, "inputAttribute", inputAttribute); + + napi_value args[] = { GetNapiBoolean(env, true) /*showkeyboard*/, attach_params, CreateNapiFunction(env, NULL, SDL_JS_ShowScreenKeyboardResult, userdata) }; + CallNapiMethod(env, ime_controller, "attach", SDL_arraysize(args), args); +} + +bool SDL_OpenHarmonyShowScreenKeyboard(int input_type, int cap_type, const SDL_Rect *text_input_rect) +{ + if (!ime_controller_ref) { + return SDL_SetError("IME controller not initialized"); + } + + ShowKeyboardData *data = (ShowKeyboardData *) SDL_calloc(1, sizeof (*data)); + if (!data) { + return false; + } + data->input_type = input_type; + data->cap_type = cap_type; + if (text_input_rect) { + SDL_copyp(&data->text_input_rect_data, text_input_rect); + data->text_input_rect = &data->text_input_rect_data; + } + + return (CallNapiThreadsafeFunction(show_screenkeyboard_threadsafefn, data, napi_tsfn_nonblocking) == napi_ok); +} + +// AsyncCallback when CallJSHideScreenKeyboard() finishes its work. +static napi_value SDL_JS_HideScreenKeyboardResult(napi_env env, napi_callback_info info) +{ + napi_value ime_controller = GetNapiRefValue(env, ime_controller_ref); + napi_value ime_oninserttext_args[] = { CreateNapiString(env, "insertText"), GetNapiRefValue(env, on_insert_text_ref) }; + CallNapiMethod(env, ime_controller, "off", SDL_arraysize(ime_oninserttext_args), ime_oninserttext_args); + napi_value ime_ondeleteleft_args[] = { CreateNapiString(env, "deleteLeft"), GetNapiRefValue(env, on_delete_left_ref) }; + CallNapiMethod(env, ime_controller, "off", SDL_arraysize(ime_ondeleteleft_args), ime_ondeleteleft_args); + SDL_SendScreenKeyboardHidden(); + return GetNapiUndefined(env); +} + +// this function is called from the main Javascript thread when it's convenient to fire it. +static void CallJSHideScreenKeyboard(napi_env env, napi_value js_callback, void *context, void *userdata) +{ + napi_value ime_controller = GetNapiRefValue(env, ime_controller_ref); + napi_value args[] = { CreateNapiFunction(env, NULL, SDL_JS_HideScreenKeyboardResult, userdata) }; + CallNapiMethod(env, ime_controller, "hideTextInput", SDL_arraysize(args), args); +} + +bool SDL_OpenHarmonyHideScreenKeyboard(void) +{ + if (!ime_controller_ref) { + return SDL_SetError("IME controller not initialized"); + } + return (CallNapiThreadsafeFunction(hide_screenkeyboard_threadsafefn, NULL, napi_tsfn_nonblocking) == napi_ok); +} + +// this function is called from the main Javascript thread when it's convenient to fire it. +static void CallJSChangeMousePointer(napi_env env, napi_value js_callback, void *context, void *userdata) +{ + const bool enable = (userdata != NULL); + napi_value pointer = GetNapiRefValue(env, pointer_ref); + napi_value args[] = { GetNapiBoolean(env, enable) }; +//SDL_Log("%s MOUSE POINTER!", enable ? "SHOW" : "HIDE"); + CallNapiMethod(env, pointer, "setPointerVisibleSync", SDL_arraysize(args), args); +} + +static bool SDL_OpenHarmonyChangeMousePointer(bool enable) +{ + if (!pointer_ref) { + return SDL_SetError("Pointer namespace not initialized"); + } + return (CallNapiThreadsafeFunction(change_mouseptr_threadsafefn, (void *) (size_t) (enable ? 0x1 : 0x0), napi_tsfn_nonblocking) == napi_ok); +} + +bool SDL_OpenHarmonyShowMousePointer(void) +{ + return SDL_OpenHarmonyChangeMousePointer(true); +} + +bool SDL_OpenHarmonyHideMousePointer(void) +{ + return SDL_OpenHarmonyChangeMousePointer(false); +} + + +// This _must_ be called from the Javascript thread, since it makes NAPI calls! +static bool UpdateSystemLocale(napi_env env) // true if known locale changed, false otherwise. +{ + if (!i18nsystem_ref) { + return false; + } + + napi_value i18n = GetNapiRefValue(env, i18nsystem_ref); + if (!i18n) { + return false; // uhoh. + } + + napi_value language = CallNapiMethod(env, i18n, "getSystemLanguage", 0, NULL); + napi_value region = CallNapiMethod(env, i18n, "getSystemRegion", 0, NULL); + + bool retval = false; + if (language && region) { + char *utf8lang = CreateSDLStringFromNAPIValue(env, language); + char *utf8region = CreateSDLStringFromNAPIValue(env, region); + if (utf8lang && utf8region) { + char *ptr = SDL_strchr(utf8lang, '-'); // if we get "en-Latn-US" or whatever, we just want "en". + if (ptr) { + *ptr = '\0'; // chop it off. + } + char *utf8locale = NULL; + if (SDL_asprintf(&utf8locale, "%s_%s", utf8lang, utf8region) > 0) { + retval = (!system_locale || (SDL_strcmp(system_locale, utf8locale) != 0)); + if (!retval) { // didn't change, free the new string. + SDL_free(utf8locale); + } else { // changed, swap them out, free the old string. + char *tmp = system_locale; + system_locale = utf8locale; + SDL_free(tmp); + } + } + } + SDL_free(utf8region); + SDL_free(utf8lang); + } + + return retval; +} + +// this function is called from the main Javascript thread when it's convenient to fire it. +static void CallJSSystemLocaleChanged(napi_env env, napi_value js_callback, void *context, void *userdata) +{ + if (UpdateSystemLocale(env)) { + SDL_SendLocaleChangedEvent(); + } +} + + +// !!! FIXME: remove the `SDL_` prefix on these static functions. + +// Callbacks into our custom XComponent. +static void SDL_XComponent_OnSurfaceCreatedCallback(OH_NativeXComponent* component, void* window) +{ + SDL_assert(native_resource_mgr != NULL); // ArkTS should have sent us this at startup. Are you using our startup scripts? + extern void SDL_OpenHarmonyMainSurfaceCreated(void); // this is in src/main/openharmony/SDL_sysmain_runapp.c + SDL_OpenHarmonyVideoSurfaceCreated(component, window); // this is in src/video/openharmony/SDL_openharmonyvideo.c + SDL_OpenHarmonyMainSurfaceCreated(); // start the actual native code app if this is the first surface. +} + +static void SDL_XComponent_OnFrameCallback(OH_NativeXComponent* component, uint64_t timestamp, uint64_t targetTimestamp) +{ + extern void SDL_OpenHarmonyOnFrameCallback(void); // this is in src/main/openharmony/SDL_sysmain_callbacks.c + SDL_OpenHarmonyOnFrameCallback(); // This fires SDL_AppInterate. +} + +static void SDL_XComponent_OnSurfaceChangedCallback(OH_NativeXComponent* component, void* window) +{ + SDL_OpenHarmonyVideoSurfaceChanged(component, window); // this is in src/video/openharmony/SDL_openharmonyvideo.c +} + +static void SDL_XComponent_OnSurfaceDestroyedCallback(OH_NativeXComponent* component, void* window) +{ + SDL_OpenHarmonyVideoSurfaceDestroyed(component, window); // this is in src/video/openharmony/SDL_openharmonyvideo.c +} + +static void SDL_XComponent_DispatchTouchEventCallback(OH_NativeXComponent* component, void* window) +{ + SDL_OpenHarmonyDispatchTouchEvent(component, window); // this is in src/video/openharmony/SDL_openharmonyvideo.c +} + +static void SDL_XComponent_DispatchMouseEventCallback(OH_NativeXComponent* component, void* window) +{ + SDL_OpenHarmonyDispatchMouseEvent(component, window); // this is in src/video/openharmony/SDL_openharmonyvideo.c +} + +static void SDL_XComponent_DispatchHoverEventCallback(OH_NativeXComponent* component, bool isHover) +{ + // !!! FIXME: use this? +} + +static void SDL_XComponent_DispatchUIInputEventCallback(OH_NativeXComponent* component, ArkUI_UIInputEvent* event, ArkUI_UIInputEvent_Type type) +{ + SDL_OpenHarmonyDispatchUIInputEvent(component, event, type); // this is in src/video/openharmony/SDL_openharmonyvideo.c +} + +static void SDL_XComponent_DispatchKeyEventCallback(OH_NativeXComponent* component, void* window) +{ + SDL_OpenHarmonyDispatchKeyEvent(component, window); +} + +static void OpenHarmonyCommonEventReceiver(const CommonEvent_RcvData *data) +{ + const char *evname = OH_CommonEvent_GetEventFromRcvData(data); + if (!evname) { + return; + } else if (SDL_strcmp(evname, COMMON_EVENT_LOCALE_CHANGED) == 0) { + // make sure we're in the javascript thread so we can call into the i18n system object via NAPI. + // this will update system_locale and then fire the SDL locale-changed event if appropriate. + CallNapiThreadsafeFunction(syslocalechanged_threadsafefn, NULL, napi_tsfn_nonblocking); + } +} + +static void UpdateDeviceOrientation(void) +{ + NativeDisplayManager_Orientation orientation = DISPLAY_MANAGER_PORTRAIT; + OH_NativeDisplayManager_GetDefaultDisplayOrientation(&orientation); + + switch (orientation) { + #define CHECKROT(ohenum, sdlenum) case DISPLAY_MANAGER_##ohenum: device_orientation = SDL_ORIENTATION_##sdlenum; break + CHECKROT(PORTRAIT, PORTRAIT); + CHECKROT(LANDSCAPE, LANDSCAPE); + CHECKROT(PORTRAIT_INVERTED, PORTRAIT_FLIPPED); + CHECKROT(LANDSCAPE_INVERTED, LANDSCAPE_FLIPPED); + CHECKROT(UNKNOWN, UNKNOWN); + #undef CHECKROT + } +} + +// Called when phone/tablet rotates to a new orientation. +static void OnDisplayChangeCallback(uint64_t displayId) +{ + uint64_t defdpyid = 0; + OH_NativeDisplayManager_GetDefaultDisplayId(&defdpyid); + if (displayId != defdpyid) { + return; // we don't care if external displays change orientation. + } + UpdateDeviceOrientation(); +} + + +// Called when windowStage.loadContent finishes. +static napi_value SDL_JS_LoadContentResult(napi_env env, napi_callback_info info) +{ + SDL_JS_ENTRY(1); + if (GetNapiInt(env, GetNapiObjField(env, argv[0], "code"), -1) != 0) { + OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Failed to load the content page! Aborting!"); + exit(1); + } + //OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Succeeded in loading the content page. Startup may now continue."); + + return GetNapiUndefined(env); +} + +// Our native version of UIAbility.onDestroy(). +static napi_value SDL_JS_UIAbility_OnDestroy(napi_env env, napi_callback_info info) +{ + SDL_FlushEvents(SDL_EVENT_FIRST, SDL_EVENT_LAST); + SDL_SendQuit(); + SDL_OnApplicationWillTerminate(); + + if (commonevent_subscriber) { + OH_CommonEvent_UnSubscribe(commonevent_subscriber); + OH_CommonEvent_DestroySubscriber(commonevent_subscriber); + commonevent_subscriber = NULL; + } + + SDL_free(system_locale); + system_locale = NULL; + + return GetNapiUndefined(env); +} + +// Our native version of UIAbility.onForeground(). +// This is for older devices; on newer ones we use OnWillForeground and OnDidForeground instead. +static napi_value SDL_JS_UIAbility_OnForeground(napi_env env, napi_callback_info info) +{ + SDL_OnApplicationWillEnterForeground(); + SDL_OnApplicationDidEnterForeground(); + return GetNapiUndefined(env); +} + +// Our native version of UIAbility.onWillForeground(). +// This is for newer devices; on older ones we use OnForeground instead. +static napi_value SDL_JS_UIAbility_OnWillForeground(napi_env env, napi_callback_info info) +{ + SDL_OnApplicationWillEnterForeground(); + return GetNapiUndefined(env); +} + +// Our native version of UIAbility.onDidForeground(). +// This is for newer devices; on older ones we use OnForeground instead. +static napi_value SDL_JS_UIAbility_OnDidForeground(napi_env env, napi_callback_info info) +{ + SDL_OnApplicationDidEnterForeground(); + return GetNapiUndefined(env); +} + +// Our native version of UIAbility.onBackground(). +// This is for older devices; on newer ones we use OnWillBackground and OnDidBackground instead. +static napi_value SDL_JS_UIAbility_OnBackground(napi_env env, napi_callback_info info) +{ + SDL_OnApplicationWillEnterBackground(); + SDL_OnApplicationDidEnterBackground(); + return GetNapiUndefined(env); +} + +// Our native version of UIAbility.onWillBackground(). +// This is for newer devices; on older ones we use OnBackground instead. +static napi_value SDL_JS_UIAbility_OnWillBackground(napi_env env, napi_callback_info info) +{ + SDL_OnApplicationWillEnterBackground(); + return GetNapiUndefined(env); +} + +// Our native version of UIAbility.onDidBackground(). +// This is for newer devices; on older ones we use OnBackground instead. +static napi_value SDL_JS_UIAbility_OnDidBackground(napi_env env, napi_callback_info info) +{ + SDL_OnApplicationDidEnterBackground(); + return GetNapiUndefined(env); +} + +// Our native version of UIAbility.onWindowStageCreate(). +static napi_value SDL_JS_UIAbility_OnWindowStageCreate(napi_env env, napi_callback_info info) +{ + // grab the window object, load "pages/Index" to continue startup. + //OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "%{public}s", SDL_FUNCTION); + SDL_JS_ENTRY(1); + napi_value windowStage = argv[0]; + napi_value window = CallNapiMethod(env, windowStage, "getMainWindowSync", 0, NULL); + napi_create_reference(env, window, 1, &window_ref); + + napi_value args[] = { CreateNapiString(env, "pages/Index"), CreateNapiFunction(env, NULL, SDL_JS_LoadContentResult, NULL) }; + CallNapiMethod(env, windowStage, "loadContent", SDL_arraysize(args), args); + + return GetNapiUndefined(env); +} + +// Our native version of UIAbility.onWindowStageDestroy(). +static napi_value SDL_JS_UIAbility_OnWindowStageDestroy(napi_env env, napi_callback_info info) +{ + return GetNapiUndefined(env); +} + +// Our native version of UIAbility.onMemoryLevel(). +static napi_value SDL_JS_UIAbility_OnMemoryLevel(napi_env env, napi_callback_info info) +{ + SDL_JS_ENTRY(1); + if (GetNapiInt(env, argv[0], 0) == 2 /*MEMORY_LEVEL_CRITICAL*/) { + SDL_OnApplicationDidReceiveMemoryWarning(); + } + return GetNapiUndefined(env); +} + +static SDL_SystemTheme GetSDLSystemThemeFromAbilityConfig(napi_env env, napi_value config) +{ + switch (GetNapiInt(env, GetNapiObjField(env, config, "colorMode"), -1)) { + case 0: return SDL_SYSTEM_THEME_DARK; + case 1: return SDL_SYSTEM_THEME_LIGHT; + default: return SDL_SYSTEM_THEME_UNKNOWN; + } +} + +// Our native version of UIAbility.onConfigurationUpdate(). +static napi_value SDL_JS_UIAbility_OnConfigurationUpdate(napi_env env, napi_callback_info info) +{ + SDL_JS_ENTRY(1); + system_theme = GetSDLSystemThemeFromAbilityConfig(env, argv[0]); + SDL_SetSystemTheme(system_theme); + return GetNapiUndefined(env); +} + +static void TakeOverUIAbility(napi_env env, napi_value ability) +{ + const int apilevel = SDL_GetOpenHarmonySDKVersion(); + napi_value prototype = GetNapiObjField(env, GetNapiObjField(env, ability, "constructor"), "prototype"); + + if (apilevel < 20) { + SetNapiObjField(env, prototype, "onForeground", CreateNapiFunction(env, "onForeground", SDL_JS_UIAbility_OnForeground, NULL)); + SetNapiObjField(env, prototype, "onBackground", CreateNapiFunction(env, "onBackground", SDL_JS_UIAbility_OnBackground, NULL)); + } else { + SetNapiObjField(env, prototype, "onWillForeground", CreateNapiFunction(env, "onWillForeground", SDL_JS_UIAbility_OnWillForeground, NULL)); + SetNapiObjField(env, prototype, "onDidForeground", CreateNapiFunction(env, "onDidForeground", SDL_JS_UIAbility_OnDidForeground, NULL)); + SetNapiObjField(env, prototype, "onWillBackground", CreateNapiFunction(env, "onWillBackground", SDL_JS_UIAbility_OnWillBackground, NULL)); + SetNapiObjField(env, prototype, "onDidBackground", CreateNapiFunction(env, "onDidBackground", SDL_JS_UIAbility_OnDidBackground, NULL)); + } + + SetNapiObjField(env, prototype, "onDestroy", CreateNapiFunction(env, "onDestroy", SDL_JS_UIAbility_OnDestroy, NULL)); + SetNapiObjField(env, prototype, "onWindowStageCreate", CreateNapiFunction(env, "onWindowStageCreate", SDL_JS_UIAbility_OnWindowStageCreate, NULL)); + SetNapiObjField(env, prototype, "onWindowStageDestroy", CreateNapiFunction(env, "onWindowStageDestroy", SDL_JS_UIAbility_OnWindowStageDestroy, NULL)); + SetNapiObjField(env, prototype, "onMemoryLevel", CreateNapiFunction(env, "onMemoryLevel", SDL_JS_UIAbility_OnMemoryLevel, NULL)); + SetNapiObjField(env, prototype, "onConfigurationUpdate", CreateNapiFunction(env, "onConfigurationUpdate", SDL_JS_UIAbility_OnConfigurationUpdate, NULL)); +} + +// ArkTS calls this once near startup to pass us the Ability, so we can call back into Javascript as necessary. +static napi_value SDL_JS_ProvideArkTSObjects(napi_env env, napi_callback_info info) +{ + SDL_assert(!native_resource_mgr); // don't call this more than once! + + // we don't bother cleaning up most things in this function, because they are intended to live as long as the process. + // !!! FIXME: but for completeness, maybe we should, in SDL_JS_UIAbility_OnDestroy(). + #define expected_argc 5 + SDL_JS_ENTRY(expected_argc); + if (argc != expected_argc) { + // if you hit this, we probably changed either this C code or the ArkTS code in EntryAbility.ets and you need to update one or both to get the back in sync. + OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "ProvideArkTSObjects: expected %{public}d objects, but got %{public}d! Script is out of sync? Aborting!", (int) expected_argc, (int) argc); + exit(1); + } + #undef expected_argc + + napi_value ability = argv[0]; + napi_value atmanager = argv[1]; + napi_value i18nsystem = argv[2]; + napi_value pointer = argv[3]; + napi_value ime_controller = argv[4]; + napi_create_reference(env, ability, 1, &ability_object_ref); + napi_create_reference(env, atmanager, 1, &atmanager_ref); + napi_create_reference(env, i18nsystem, 1, &i18nsystem_ref); + napi_create_reference(env, pointer, 1, &pointer_ref); + napi_create_reference(env, ime_controller, 1, &ime_controller_ref); + + TakeOverUIAbility(env, ability); + + napi_value context = GetNapiObjField(env, ability, "context"); + napi_value resourceManager = GetNapiObjField(env, context, "resourceManager"); + native_resource_mgr = OH_ResourceManager_InitNativeResourceManager(env, resourceManager); + napi_value config = GetNapiObjField(env, context, "config"); + + system_theme = GetSDLSystemThemeFromAbilityConfig(env, config); + + napi_value on_insert_text = CreateNapiFunction(env, "onInsertText", SDL_JS_IME_Controller_OnInsertText, NULL); + napi_create_reference(env, on_insert_text, 1, &on_insert_text_ref); + napi_value on_delete_left = CreateNapiFunction(env, "onDeleteLeft", SDL_JS_IME_Controller_OnDeleteLeft, NULL); + napi_create_reference(env, on_delete_left, 1, &on_delete_left_ref); + + UpdateSystemLocale(env); // do this at startup, so we have it saved off while we know we're on the Javascript thread. + + // the Video subsystem will also register one of these for tracking displays, but this is so we can track device orientation changes independently. + uint32_t display_change_listener_idx = 0; + OH_NativeDisplayManager_RegisterDisplayChangeListener(OnDisplayChangeCallback, &display_change_listener_idx); + UpdateDeviceOrientation(); + + // Set up some threadsafe functions, for calling back into ArkTS from the main thread, regardless of what thread native code is operating from. + syslocalechanged_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_SystemLocaleChanged", CallJSSystemLocaleChanged); + req_permissions_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_RequestOpenHarmonyPermission", CallJSRequestPermissions); + open_url_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyOpenURL", CallJSOpenURL); + change_sysbars_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyChangeSystemBars", CallJSChangeSysBars); + change_screensaver_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyChangeScreenSaver", CallJSChangeScreenSaver); + show_screenkeyboard_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyShowScreenKeyboard", CallJSShowScreenKeyboard); + hide_screenkeyboard_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyHideScreenKeyboard", CallJSHideScreenKeyboard); + change_mouseptr_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyChangeMousePointer", CallJSChangeMousePointer); + + return GetNapiUndefined(env); +} + + +// This is called by SDL_RegisterNativeInterfaces when the library is loaded, which sets up the entry points where +// ArkTS code can call into our native code. +static napi_value SDL_Init_Native_Interfaces(napi_env env, napi_value exports) +{ + // Functions that we want to be able to call from ArkTS go here. + // (declare them in C as `napi_value MyFunctionName(napi_env env, napi_callback_info info);`) + napi_property_descriptor desc[] = { + { "provideArkTSObjects", NULL, SDL_JS_ProvideArkTSObjects, NULL, NULL, NULL, napi_default, NULL }, + }; + napi_define_properties(env, exports, SDL_arraysize(desc), desc); + + // Wire into our XComponent, so we can take control from C code. If any of this fails, I assume the app will either blow up or do nothing. + napi_value xcompobj = GetNapiObjField(env, exports, OH_NATIVE_XCOMPONENT_OBJ); + if (!xcompobj) { + return exports; + } + + OH_NativeXComponent *nativeXComponent = NULL; + if (napi_unwrap(env, xcompobj, (void **) &nativeXComponent) != napi_ok) { + return exports; + } + + static OH_NativeXComponent_Callback xcomp_callbacks = { // this MUST be static! It keeps a pointer to this, and doesn't make a copy, afaict! + .OnSurfaceCreated = SDL_XComponent_OnSurfaceCreatedCallback, + .OnSurfaceChanged = SDL_XComponent_OnSurfaceChangedCallback, + .OnSurfaceDestroyed = SDL_XComponent_OnSurfaceDestroyedCallback, + .DispatchTouchEvent = SDL_XComponent_DispatchTouchEventCallback + }; + OH_NativeXComponent_RegisterCallback(nativeXComponent, &xcomp_callbacks); + OH_NativeXComponent_RegisterOnFrameCallback(nativeXComponent, SDL_XComponent_OnFrameCallback); + + static OH_NativeXComponent_MouseEvent_Callback xcomp_mouse_callbacks = { + .DispatchMouseEvent = SDL_XComponent_DispatchMouseEventCallback, + .DispatchHoverEvent = SDL_XComponent_DispatchHoverEventCallback + }; + OH_NativeXComponent_RegisterMouseEventCallback(nativeXComponent, &xcomp_mouse_callbacks); + + // only AXIS events supported here, at the moment, apparently, but most of the other things (mouse, touch, key) come through other supported callbacks. + // "Axis" in this case only means mousewheel, afaict. + OH_NativeXComponent_RegisterUIInputEventCallback(nativeXComponent, SDL_XComponent_DispatchUIInputEventCallback, ARKUI_UIINPUTEVENT_TYPE_AXIS); + + OH_NativeXComponent_RegisterKeyEventCallback(nativeXComponent, SDL_XComponent_DispatchKeyEventCallback); + + static const char * const common_events[] = { COMMON_EVENT_LOCALE_CHANGED }; + CommonEvent_SubscribeInfo *subinfo = OH_CommonEvent_CreateSubscribeInfo((const char **) common_events, SDL_arraysize(common_events)); + if (subinfo) { + commonevent_subscriber = OH_CommonEvent_CreateSubscriber(subinfo, OpenHarmonyCommonEventReceiver); + if (commonevent_subscriber) { + OH_CommonEvent_Subscribe(commonevent_subscriber); + } + OH_CommonEvent_DestroySubscribeInfo(subinfo); + } + + return exports; +} + +// This runs when libSDL3.so loads, and registers the NAPI module, so ArkTS can call into this to get going. +void __attribute__((constructor)) SDL_RegisterNativeInterfaces(void) +{ + static napi_module sdl_napi_module = { + .nm_version = 1, + .nm_flags = 0, + .nm_filename = NULL, + .nm_register_func = SDL_Init_Native_Interfaces, + .nm_modname = "SDL3", + .nm_priv = ((void*)0), + .reserved = { 0 }, + }; + napi_module_register(&sdl_napi_module); +} + +#endif // SDL_PLATFORM_OPENHARMONY + diff --git a/src/core/openharmony/SDL_openharmony.h b/src/core/openharmony/SDL_openharmony.h new file mode 100644 index 0000000000..a3624f7310 --- /dev/null +++ b/src/core/openharmony/SDL_openharmony.h @@ -0,0 +1,66 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_openharmony_h +#define SDL_openharmony_h + +// Set up for C function definitions, even when using C++ +#ifdef __cplusplus +/* *INDENT-OFF* */ +extern "C" { +/* *INDENT-ON* */ +#endif + +void SDL_DebugLogOpenHarmonyInfo(void); +const char *SDL_GetOpenHarmonySystemLocale(void); +SDL_SystemTheme SDL_GetOpenHarmonySystemTheme(void); +SDL_DisplayOrientation SDL_GetOpenHarmonyDeviceCurrentOrientation(void); +SDL_FormFactor SDL_GetOpenHarmonyDeviceFormFactor(void); + +bool SDL_OpenHarmonyOpenURL(const char *url); +bool SDL_OpenHarmonyToggleSystemBars(bool status_bar, bool navigation_bar); +bool SDL_OpenHarmonyChangeScreenSaver(bool enable); +bool SDL_OpenHarmonyShowScreenKeyboard(int input_type, int cap_type, const SDL_Rect *text_input_rect); +bool SDL_OpenHarmonyHideScreenKeyboard(void); +bool SDL_OpenHarmonyShowMousePointer(void); +bool SDL_OpenHarmonyHideMousePointer(void); + +bool SDL_OpenHarmonyRawFileOpen(void **puserdata, const char *fileName, const char *mode); +Sint64 SDL_OpenHarmonyRawFileSize(void *userdata); +Sint64 SDL_OpenHarmonyRawFileSeek(void *userdata, Sint64 offset, SDL_IOWhence whence); +size_t SDL_OpenHarmonyRawFileRead(void *userdata, void *buffer, size_t size, SDL_IOStatus *status); +bool SDL_OpenHarmonyRawFileClose(void *userdata); +bool SDL_OpenHarmonyEnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata); +bool SDL_OpenHarmonyGetAssetPathInfo(const char *path, SDL_PathInfo *info); + +#define SDL_PlatformEnumerateAssetDirectory SDL_OpenHarmonyEnumerateAssetDirectory +#define SDL_GetPlatformInternalStoragePath SDL_GetOpenHarmonyInternalStoragePath +#define SDL_PlatformGetAssetPathInfo SDL_OpenHarmonyGetAssetPathInfo + +// Ends C function definitions when using C++ +#ifdef __cplusplus +/* *INDENT-OFF* */ +} +/* *INDENT-ON* */ +#endif + +#endif // SDL_openharmony_h diff --git a/src/cpuinfo/SDL_cpuinfo.c b/src/cpuinfo/SDL_cpuinfo.c index d58c150317..d343592848 100644 --- a/src/cpuinfo/SDL_cpuinfo.c +++ b/src/cpuinfo/SDL_cpuinfo.c @@ -493,7 +493,7 @@ static int CPU_haveNEON(void) return 0; } return (hasneon & HWCAP_NEON) == HWCAP_NEON; -#elif (defined(SDL_PLATFORM_LINUX) && defined(HAVE_GETAUXVAL)) || defined(SDL_PLATFORM_ANDROID) +#elif (defined(SDL_PLATFORM_LINUX) && defined(HAVE_GETAUXVAL)) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) return (getauxval(AT_HWCAP) & HWCAP_NEON) == HWCAP_NEON; #elif defined(SDL_PLATFORM_LINUX) return readProcAuxvForNeon(); diff --git a/src/dynapi/SDL_dynapi.exports b/src/dynapi/SDL_dynapi.exports index 5d5df5d17c..3ff5e7c637 100644 --- a/src/dynapi/SDL_dynapi.exports +++ b/src/dynapi/SDL_dynapi.exports @@ -1311,3 +1311,6 @@ _SDL_SetRenderViewportFloat _SDL_GetRenderViewportFloat _SDL_SetRenderClipRectFloat _SDL_GetRenderClipRectFloat +_SDL_GetOpenHarmonySDKVersion +_SDL_GetOpenHarmonyInternalStoragePath +_SDL_RequestOpenHarmonyPermission diff --git a/src/dynapi/SDL_dynapi.h b/src/dynapi/SDL_dynapi.h index 2713e3f464..04c310ce29 100644 --- a/src/dynapi/SDL_dynapi.h +++ b/src/dynapi/SDL_dynapi.h @@ -49,6 +49,8 @@ #define SDL_DYNAMIC_API 0 #elif defined(SDL_PLATFORM_ANDROID) // probably not useful on Android. #define SDL_DYNAMIC_API 0 +#elif defined(SDL_PLATFORM_OPENHARMONY) // probably not useful on OpenHarmony. +#define SDL_DYNAMIC_API 0 #elif defined(SDL_PLATFORM_EMSCRIPTEN) // probably not useful on Emscripten. #define SDL_DYNAMIC_API 0 #elif defined(SDL_PLATFORM_PS2) && SDL_PLATFORM_PS2 diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 63864e372b..f9b5a748cb 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1312,6 +1312,9 @@ SDL3_0.0.0 { SDL_GetRenderViewportFloat; SDL_SetRenderClipRectFloat; SDL_GetRenderClipRectFloat; + SDL_GetOpenHarmonySDKVersion; + SDL_GetOpenHarmonyInternalStoragePath; + SDL_RequestOpenHarmonyPermission; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index d8deffc20e..f6ce69d83d 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1338,3 +1338,6 @@ #define SDL_GetRenderViewportFloat SDL_GetRenderViewportFloat_REAL #define SDL_SetRenderClipRectFloat SDL_SetRenderClipRectFloat_REAL #define SDL_GetRenderClipRectFloat SDL_GetRenderClipRectFloat_REAL +#define SDL_GetOpenHarmonySDKVersion SDL_GetOpenHarmonySDKVersion_REAL +#define SDL_GetOpenHarmonyInternalStoragePath SDL_GetOpenHarmonyInternalStoragePath_REAL +#define SDL_RequestOpenHarmonyPermission SDL_RequestOpenHarmonyPermission_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index cd2fc200a2..2eeb1f3879 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1346,3 +1346,6 @@ SDL_DYNAPI_PROC(bool,SDL_SetRenderViewportFloat,(SDL_Renderer *a,const SDL_FRect SDL_DYNAPI_PROC(bool,SDL_GetRenderViewportFloat,(SDL_Renderer *a,SDL_FRect *b),(a,b),return) SDL_DYNAPI_PROC(bool,SDL_SetRenderClipRectFloat,(SDL_Renderer *a,const SDL_FRect *b),(a,b),return) SDL_DYNAPI_PROC(bool,SDL_GetRenderClipRectFloat,(SDL_Renderer *a,SDL_FRect *b),(a,b),return) +SDL_DYNAPI_PROC(int,SDL_GetOpenHarmonySDKVersion,(void),(),return) +SDL_DYNAPI_PROC(const char*,SDL_GetOpenHarmonyInternalStoragePath,(void),(),return) +SDL_DYNAPI_PROC(bool,SDL_RequestOpenHarmonyPermission,(const char *a,SDL_RequestOpenHarmonyPermissionCallback b,void *c),(a,b,c),return) diff --git a/src/events/SDL_keyboard.c b/src/events/SDL_keyboard.c index 53508fd53f..796ca3dbde 100644 --- a/src/events/SDL_keyboard.c +++ b/src/events/SDL_keyboard.c @@ -333,7 +333,7 @@ SDL_Window *SDL_GetKeyboardFocus(void) bool SDL_SetKeyboardFocus(SDL_Window *window) { -#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID) +#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID) && !defined(SDL_PLATFORM_OPENHARMONY) SDL_VideoDevice *video = SDL_GetVideoDevice(); #endif SDL_Keyboard *keyboard = &SDL_keyboard; @@ -354,7 +354,7 @@ bool SDL_SetKeyboardFocus(SDL_Window *window) if (keyboard->focus && keyboard->focus != window) { SDL_SendWindowEvent(keyboard->focus, SDL_EVENT_WINDOW_FOCUS_LOST, 0, 0); -#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID) +#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID) && !defined(SDL_PLATFORM_OPENHARMONY) // Ensures IME compositions are committed if (SDL_TextInputActive(keyboard->focus)) { if (video && video->StopTextInput) { @@ -384,7 +384,7 @@ bool SDL_SetKeyboardFocus(SDL_Window *window) if (keyboard->focus) { SDL_SendWindowEvent(keyboard->focus, SDL_EVENT_WINDOW_FOCUS_GAINED, 0, 0); -#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID) +#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID) && !defined(SDL_PLATFORM_OPENHARMONY) if (SDL_TextInputActive(keyboard->focus)) { if (video && video->StartTextInput) { video->StartTextInput(video, keyboard->focus, keyboard->focus->text_input_props); diff --git a/src/filesystem/SDL_filesystem.c b/src/filesystem/SDL_filesystem.c index bde66cdae5..3c065acadd 100644 --- a/src/filesystem/SDL_filesystem.c +++ b/src/filesystem/SDL_filesystem.c @@ -375,7 +375,7 @@ char **SDL_InternalGlobDirectory(const char *path, const char *pattern, SDL_Glob size_t pathlen = SDL_strlen(path); // if path ends with any slash, chop them off, so we don't confuse the pattern matcher later. - #ifdef SDL_PLATFORM_ANDROID + #if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) if (SDL_strcmp(path, "assets://") == 0) { // don't chop '//' off this if we're looking for the root of the asset tree. pathlen--; // we'll add a 1 again later. } else diff --git a/src/filesystem/openharmony/SDL_sysfilesystem.c b/src/filesystem/openharmony/SDL_sysfilesystem.c new file mode 100644 index 0000000000..5c657a7757 --- /dev/null +++ b/src/filesystem/openharmony/SDL_sysfilesystem.c @@ -0,0 +1,78 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_FILESYSTEM_OPENHARMONY + + +// !!! FIXME: HACK to prevent from including. It has '&' instead of '*' for some args, which suggests it's only been tested with C++. +#define ABILITY_RUNTIME_START_OPTIONS_H +typedef enum AbilityRuntime_StartOptions AbilityRuntime_StartOptions; + + +#include +#include + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +// System dependent filesystem routines + +#include "../SDL_sysfilesystem.h" + +char *SDL_SYS_GetBasePath(void) +{ + return SDL_strdup("assets://"); +} + +char *SDL_SYS_GetExeName(void) +{ + char buffer[128]; + int32_t writelen = 0; + const AbilityRuntime_ErrorCode rc = OH_AbilityRuntime_ApplicationContextGetBundleName(buffer, (int32_t) sizeof (buffer), &writelen); + if (rc != ABILITY_RUNTIME_ERROR_CODE_NO_ERROR) { + SDL_SetError("OH_AbilityRuntime_ApplicationContextGetBundleName failed: %d", (int) rc); + return NULL; + } + return SDL_strdup(buffer); +} + +char *SDL_SYS_GetPrefPath(const char *org, const char *app) +{ + const char *prefdir = SDL_GetOpenHarmonyInternalStoragePath(); + char *retval = NULL; + if (prefdir) { + if (SDL_asprintf(&retval, "%s/%s/", prefdir, app) < 0) { + retval = NULL; + } + } + + mkdir(prefdir, 0755); + mkdir(retval, 0755); + + return retval; +} + +char *SDL_SYS_GetUserFolder(SDL_Folder folder) +{ + SDL_Unsupported(); // !!! FIXME: Can we support _any_ of this? + return NULL; +} + +#endif // SDL_FILESYSTEM_OPENHARMONY diff --git a/src/filesystem/posix/SDL_sysfsops.c b/src/filesystem/posix/SDL_sysfsops.c index 41dbf63344..b473dfbfc7 100644 --- a/src/filesystem/posix/SDL_sysfsops.c +++ b/src/filesystem/posix/SDL_sysfsops.c @@ -39,24 +39,28 @@ #include "../../core/android/SDL_android.h" #endif +#ifdef SDL_PLATFORM_OPENHARMONY +#include "../../core/openharmony/SDL_openharmony.h" +#endif + bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata) { char *apath = NULL; // absolute path (for Android, iOS, etc). Overrides `path`. -#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_IOS) +#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_OPENHARMONY) if (*path == '\0') { return SDL_SetError("No such file or directory"); } else if (*path != '/') { - #ifdef SDL_PLATFORM_ANDROID + #if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) if (SDL_strncmp(path, "assets://", 9) == 0) { char *pathwithsep = NULL; SDL_asprintf(&pathwithsep, "%s%s", path, (path[SDL_strlen(path) - 1] != '/') ? "/" : ""); - const bool retval = pathwithsep ? Android_JNI_EnumerateAssetDirectory(pathwithsep, cb, userdata) : false; + const bool retval = pathwithsep ? SDL_PlatformEnumerateAssetDirectory(pathwithsep, cb, userdata) : false; SDL_free(pathwithsep); return retval; } - SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path); + SDL_asprintf(&apath, "%s/%s", SDL_GetPlatformInternalStoragePath(), path); #elif defined(SDL_PLATFORM_IOS) char *base = SDL_GetPrefPath("", ""); if (!base) { @@ -71,7 +75,7 @@ bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback return false; } } -#elif 0 // this is just for testing that `apath` works when you aren't on iOS or Android. +#elif 0 // this is just for testing that `apath` works when you aren't on iOS or Android or HarmonyOS. if (*path != '/') { char *c = SDL_SYS_GetCurrentDirectory(); SDL_asprintf(&apath, "%s%s", c, path); @@ -98,9 +102,9 @@ bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback DIR *dir = opendir(pathwithsep); if (!dir) { -#ifdef SDL_PLATFORM_ANDROID // Maybe it's an asset... that didn't use an "assets://" URL? +#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) // Maybe it's an asset... that didn't use an "assets://" URL? if (*pathwithsep != '/') { // don't fall back to asset tree for absolute paths, in case opendir() failed for other reasons, like opendir("/") returning EACCES. - const bool retval = Android_JNI_EnumerateAssetDirectory(pathwithsep + extralen, cb, userdata); + const bool retval = SDL_PlatformEnumerateAssetDirectory(pathwithsep + extralen, cb, userdata); SDL_free(pathwithsep); return retval; } @@ -130,12 +134,12 @@ bool SDL_SYS_RemovePath(const char *path) { int rc; -#ifdef SDL_PLATFORM_ANDROID +#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) if (*path == '/') { rc = remove(path); } else { char *apath = NULL; - SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path); + SDL_asprintf(&apath, "%s/%s", SDL_GetPlatformInternalStoragePath(), path); if (!apath) { return false; } @@ -177,18 +181,18 @@ bool SDL_SYS_RenamePath(const char *oldpath, const char *newpath) { int rc; -#ifdef SDL_PLATFORM_ANDROID +#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) char *aoldpath = NULL; char *anewpath = NULL; if (*oldpath != '/') { - SDL_asprintf(&aoldpath, "%s/%s", SDL_GetAndroidInternalStoragePath(), oldpath); + SDL_asprintf(&aoldpath, "%s/%s", SDL_GetPlatformInternalStoragePath(), oldpath); if (!aoldpath) { return false; } oldpath = aoldpath; } if (*newpath != '/') { - SDL_asprintf(&anewpath, "%s/%s", SDL_GetAndroidInternalStoragePath(), newpath); + SDL_asprintf(&anewpath, "%s/%s", SDL_GetPlatformInternalStoragePath(), newpath); if (!anewpath) { SDL_free(aoldpath); return false; @@ -349,16 +353,16 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) struct stat statbuf; int rc; -#ifdef SDL_PLATFORM_ANDROID +#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) if (*path == '\0') { return SDL_SetError("No such file or directory"); } else if (*path == '/') { rc = stat(path, &statbuf); } else if (SDL_strncmp(path, "assets://", 9) == 0) { - return Android_JNI_GetAssetPathInfo(path, info); + return SDL_PlatformGetAssetPathInfo(path, info); } else { char *apath = NULL; - SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path); + SDL_asprintf(&apath, "%s/%s", SDL_GetPlatformInternalStoragePath(), path); if (!apath) { return false; } @@ -366,7 +370,7 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) SDL_free(apath); } if (rc < 0) { // Maybe it's an asset... that didn't use an "assets://" URL? - return Android_JNI_GetAssetPathInfo(path, info); + return SDL_PlatformGetAssetPathInfo(path, info); } #elif defined(SDL_PLATFORM_IOS) if (*path == '/') { diff --git a/src/io/SDL_iostream.c b/src/io/SDL_iostream.c index 60caf33166..3382201b04 100644 --- a/src/io/SDL_iostream.c +++ b/src/io/SDL_iostream.c @@ -69,6 +69,11 @@ struct SDL_IOStream #include "../core/android/SDL_android.h" #endif +#ifdef SDL_PLATFORM_OPENHARMONY +#include +#include "../core/openharmony/SDL_openharmony.h" +#endif + #if defined(SDL_PLATFORM_WINDOWS) && !defined(SDL_PLATFORM_CYGWIN) typedef struct IOStreamWindowsData @@ -1024,7 +1029,7 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode) return NULL; } -#ifdef SDL_PLATFORM_ANDROID +#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) #ifdef HAVE_STDIO_H // Try to open the file on the filesystem first if (*file == '/') { @@ -1037,6 +1042,7 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode) } return SDL_IOFromFP(fp, true); } + #ifdef SDL_PLATFORM_ANDROID } else if (SDL_strncmp(file, "content://", 10) == 0) { // Try opening content:// URI int fd = Android_JNI_OpenFileDescriptor(file, mode); @@ -1053,10 +1059,11 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode) } return SDL_IOFromFP(fp, true); + #endif } else if (SDL_strncmp(file, "assets://", 9) != 0) { // Try opening it from internal storage if it's a relative path char *path = NULL; - SDL_asprintf(&path, "%s/%s", SDL_GetAndroidInternalStoragePath(), file); + SDL_asprintf(&path, "%s/%s", SDL_GetPlatformInternalStoragePath(), file); if (path) { FILE *fp = fopen(path, mode); SDL_free(path); @@ -1072,6 +1079,7 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode) } #endif // HAVE_STDIO_H + #ifdef SDL_PLATFORM_ANDROID // Try to open the file from the asset system? void *iodata = NULL; if (!Android_JNI_FileOpen(&iodata, file, mode)) { @@ -1093,6 +1101,32 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode) iostr->setioprops = android_setioprops; } + #elif defined(SDL_PLATFORM_OPENHARMONY) + // Try to open the file from the asset system? + void *iodata = NULL; + if (!SDL_OpenHarmonyRawFileOpen(&iodata, file, mode)) { + return NULL; + } + + SDL_IOStreamInterface iface; + SDL_INIT_INTERFACE(&iface); + iface.size = SDL_OpenHarmonyRawFileSize; + iface.seek = SDL_OpenHarmonyRawFileSeek; + iface.read = SDL_OpenHarmonyRawFileRead; + iface.write = NULL; // no write access via RawFile. + iface.close = SDL_OpenHarmonyRawFileClose; + + iostr = SDL_OpenIO(&iface, iodata); + if (!iostr) { + iface.close(iodata); + } else { + const SDL_PropertiesID props = SDL_GetIOProperties(iostr); + if (props) { + SDL_SetPointerProperty(props, SDL_PROP_IOSTREAM_OPENHARMONY_RAWFILE64_POINTER, iodata); + } + } + #endif + #elif defined(SDL_PLATFORM_IOS) // Try to open the file on the filesystem first diff --git a/src/locale/openharmony/SDL_syslocale.c b/src/locale/openharmony/SDL_syslocale.c new file mode 100644 index 0000000000..c7a9edb543 --- /dev/null +++ b/src/locale/openharmony/SDL_syslocale.c @@ -0,0 +1,36 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" +#include "../SDL_syslocale.h" + +#include "../../core/openharmony/SDL_openharmony.h" + +bool SDL_SYS_GetPreferredLocales(char *buf, size_t buflen) +{ + const char *syslocale = SDL_GetOpenHarmonySystemLocale(); + if (!syslocale) { + return SDL_SetError("Current locale couldn't be determined"); + } + SDL_strlcpy(buf, syslocale, buflen); + return true; +} + diff --git a/src/locale/psp/SDL_syslocale.c b/src/locale/psp/SDL_syslocale.c index b6ec4f70dd..3a3975be27 100644 --- a/src/locale/psp/SDL_syslocale.c +++ b/src/locale/psp/SDL_syslocale.c @@ -75,4 +75,3 @@ bool SDL_SYS_GetPreferredLocales(char *buf, size_t buflen) return true; } -/* vi: set ts=4 sw=4 expandtab: */ diff --git a/src/main/SDL_main_callbacks.c b/src/main/SDL_main_callbacks.c index 757baef0ca..3b4a206831 100644 --- a/src/main/SDL_main_callbacks.c +++ b/src/main/SDL_main_callbacks.c @@ -88,10 +88,7 @@ static bool SDLCALL SDL_MainCallbackEventWatcher(void *userdata, SDL_Event *even bool SDL_HasMainCallbacks(void) { - if (SDL_main_iteration_callback) { - return true; - } - return false; + return (SDL_main_iteration_callback != NULL); } SDL_AppResult SDL_InitMainCallbacks(int argc, char *argv[], SDL_AppInit_func appinit, SDL_AppIterate_func appiter, SDL_AppEvent_func appevent, SDL_AppQuit_func appquit) @@ -107,9 +104,7 @@ SDL_AppResult SDL_InitMainCallbacks(int argc, char *argv[], SDL_AppInit_func app if (!SDL_InitSubSystem(SDL_INIT_EVENTS)) { SDL_SetAtomicInt(&apprc, SDL_APP_FAILURE); return SDL_APP_FAILURE; - } - - if (!SDL_AddEventWatch(SDL_MainCallbackEventWatcher, NULL)) { + } else if (!SDL_AddEventWatch(SDL_MainCallbackEventWatcher, NULL)) { SDL_SetAtomicInt(&apprc, SDL_APP_FAILURE); return SDL_APP_FAILURE; } diff --git a/src/main/generic/SDL_sysmain_callbacks.c b/src/main/generic/SDL_sysmain_callbacks.c index 425e79077c..3ac645411d 100644 --- a/src/main/generic/SDL_sysmain_callbacks.c +++ b/src/main/generic/SDL_sysmain_callbacks.c @@ -23,6 +23,7 @@ #include "../SDL_main_callbacks.h" #include "../../video/SDL_sysvideo.h" +// !!! FIXME: does this ifndef need to be here? #ifndef SDL_PLATFORM_IOS static Uint64 callback_rate_increment = 0; diff --git a/src/main/openharmony/SDL_sysmain_callbacks.c b/src/main/openharmony/SDL_sysmain_callbacks.c new file mode 100644 index 0000000000..520ab5143f --- /dev/null +++ b/src/main/openharmony/SDL_sysmain_callbacks.c @@ -0,0 +1,60 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifdef SDL_PLATFORM_OPENHARMONY + +#include "../SDL_main_callbacks.h" + +void SDL_OpenHarmonyOnFrameCallback(void) // src/core/openharmony calls this when our XComponent is ready for a new frame, and we call SDL_AppIterate here +{ + if (SDL_HasMainCallbacks()) { // ignore this if we're using SDL_main(). + const SDL_AppResult rc = SDL_IterateMainCallbacks(true); + if (rc != SDL_APP_CONTINUE) { + SDL_Log("SDL_AppIterate said %s! Terminating!", (rc == SDL_APP_FAILURE) ? "SDL_APP_FAILURE" : "SDL_APP_SUCCESS"); + SDL_QuitMainCallbacks(rc); + exit((rc == SDL_APP_FAILURE) ? 1 : 0); + } + } +} + +// We hook into this deep into the actual OpenHarmony app lifecycle, see notes elsewhere. Once we return from this, we'll get OnFrame callbacks from our +// XComponent, which we'll use for SDL_AppIterate, etc. +int SDL_EnterAppMainCallbacks(int argc, char *argv[], SDL_AppInit_func appinit, SDL_AppIterate_func appiter, SDL_AppEvent_func appevent, SDL_AppQuit_func appquit) +{ + const SDL_AppResult rc = SDL_InitMainCallbacks(argc, argv, appinit, appiter, appevent, appquit); + if (rc == SDL_APP_CONTINUE) { +SDL_Log("SDL_AppInit said continue! We are live!"); + return 0; // this will fall all the way out of SDL_main (or whatever), where OpenHarmony will keep running the process, passing events through ArkTS, etc... + } + +SDL_Log("SDL_AppInit said %s! Terminating!", (rc == SDL_APP_FAILURE) ? "SDL_APP_FAILURE" : "SDL_APP_SUCCESS"); + + // appinit requested quit, just bounce out now. + SDL_QuitMainCallbacks(rc); + exit((rc == SDL_APP_FAILURE) ? 1 : 0); + + return 1; // just in case. +} + +#endif + diff --git a/src/main/openharmony/SDL_sysmain_runapp.c b/src/main/openharmony/SDL_sysmain_runapp.c new file mode 100644 index 0000000000..7feca8b80d --- /dev/null +++ b/src/main/openharmony/SDL_sysmain_runapp.c @@ -0,0 +1,102 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifdef SDL_PLATFORM_OPENHARMONY + +#include +#include +#include +#include +#include + +#include "../SDL_main_callbacks.h" + +static int SDL_main_argc = 0; +static char **SDL_main_argv = NULL; +static void *ThreadEntry__SDL_main(void *userdata) +{ + SDL_main_func pMain = (SDL_main_func) userdata; + pthread_setname_np(pthread_self(), "SDL_main"); + OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Calling SDL_main!"); + const int rc = pMain(SDL_main_argc, SDL_main_argv); + OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "SDL_main returned %{public}d! Now terminating the process...", rc); + exit(rc); + return NULL; +} + +// This is passed to SDL_RunApp. It decides whether libmain.so has the main callbacks or SDL_main, +// and either calls SDL_EnterAppMainCallbacks or spins a thread and attempts to work with SDL_main(). +// This will log something and attempt to terminate the process if things go wrong. +static int RunAppOpenHarmonyMain(int argc, char **argv) +{ + // don't call SDL_Log() in here, we aren't necessarily set up yet! Use hilog directly!! + + const char *sofile = "libmain.so"; + void *lib = dlopen(sofile, RTLD_NOW | RTLD_LOCAL); + if (!lib) { + OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Failed loading %{public}s, can't start app! (%{public}s)", sofile, dlerror()); + exit(1); + } + + SDL_main_func pMain = (SDL_main_func) dlsym(lib, "SDL_main"); + if (pMain) { + SDL_main_argc = argc; + SDL_main_argv = argv; + pthread_t thread; + const int rc = pthread_create(&thread, NULL, ThreadEntry__SDL_main, pMain); + if (rc != 0) { + OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Failed to create thread to run SDL_main from %{public}s, can't start app! (%{public}s)", sofile, strerror(rc)); + exit(1); + } + + pthread_detach(thread); // we won't be waiting on this. + return 0; // SDL_main is running in another thread, and this thread returns to the app's core loop thing. + } + + #define FINDCBFN(nam) \ + SDL_App##nam##_func pApp##nam = (SDL_App##nam##_func) dlsym(lib, "SDL_App" #nam); \ + if (!pApp##nam) { \ + OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Couldn't find %{public}s in %{public}s, can't start app! (%{public}s)", "SDL_App" #nam, sofile, dlerror()); \ + exit(1); \ + } + FINDCBFN(Init); + FINDCBFN(Iterate); + FINDCBFN(Event); + FINDCBFN(Quit); + #undef FINDCBFN + + // this returns after SDL_AppInit, and we'll deal with SDL_AppEvent/SDL_AppIterate in SDL_sysmain_callbacks.c, when we get OnFrame events from SDL's XComponent. + return SDL_EnterAppMainCallbacks(argc, argv, pAppInit, pAppIterate, pAppEvent, pAppQuit); +} + +void SDL_OpenHarmonyMainSurfaceCreated(void) // src/core/openharmony calls this when our XComponent is ready to go, and we use this to get the actual native app code booted. +{ + static bool booted = false; + if (!booted) { // just in case we make more XComponents later...? + booted = true; + SDL_RunApp(0, NULL, RunAppOpenHarmonyMain, NULL); + } +} + +#endif + diff --git a/src/misc/android/SDL_sysurl.c b/src/misc/android/SDL_sysurl.c index 8ed17a4d42..c3dcdc28bb 100644 --- a/src/misc/android/SDL_sysurl.c +++ b/src/misc/android/SDL_sysurl.c @@ -20,6 +20,8 @@ */ #include "SDL_internal.h" +#if defined(SDL_PLATFORM_ANDROID) + #include "../SDL_sysurl.h" #include "../../core/android/SDL_android.h" @@ -27,3 +29,6 @@ bool SDL_SYS_OpenURL(const char *url) { return Android_JNI_OpenURL(url); } + +#endif + diff --git a/src/misc/openharmony/SDL_sysurl.c b/src/misc/openharmony/SDL_sysurl.c new file mode 100644 index 0000000000..ae3d2f3f7a --- /dev/null +++ b/src/misc/openharmony/SDL_sysurl.c @@ -0,0 +1,34 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#if defined(SDL_PLATFORM_OPENHARMONY) + +#include "../SDL_sysurl.h" +#include "../../core/openharmony/SDL_openharmony.h" + +bool SDL_SYS_OpenURL(const char *url) +{ + return SDL_OpenHarmonyOpenURL(url); +} + +#endif + diff --git a/src/power/SDL_power.c b/src/power/SDL_power.c index 8308890934..b950236440 100644 --- a/src/power/SDL_power.c +++ b/src/power/SDL_power.c @@ -62,6 +62,9 @@ static SDL_GetPowerInfo_Impl implementations[] = { #ifdef SDL_POWER_ANDROID // handles Android. SDL_GetPowerInfo_Android, #endif +#ifdef SDL_POWER_OPENHARMONY // handles HarmonyOS/OpenHarmony + SDL_GetPowerInfo_OpenHarmony, +#endif #ifdef SDL_POWER_PSP // handles PSP. SDL_GetPowerInfo_PSP, #endif diff --git a/src/power/SDL_syspower.h b/src/power/SDL_syspower.h index 6ef3e26b2c..ac54b7dc02 100644 --- a/src/power/SDL_syspower.h +++ b/src/power/SDL_syspower.h @@ -35,6 +35,7 @@ bool SDL_GetPowerInfo_UIKit(SDL_PowerState *, int *, int *); bool SDL_GetPowerInfo_MacOSX(SDL_PowerState *, int *, int *); bool SDL_GetPowerInfo_Haiku(SDL_PowerState *, int *, int *); bool SDL_GetPowerInfo_Android(SDL_PowerState *, int *, int *); +bool SDL_GetPowerInfo_OpenHarmony(SDL_PowerState *, int *, int *); bool SDL_GetPowerInfo_PSP(SDL_PowerState *, int *, int *); bool SDL_GetPowerInfo_VITA(SDL_PowerState *, int *, int *); bool SDL_GetPowerInfo_N3DS(SDL_PowerState *, int *, int *); diff --git a/src/power/openharmony/SDL_syspower.c b/src/power/openharmony/SDL_syspower.c new file mode 100644 index 0000000000..02caf37768 --- /dev/null +++ b/src/power/openharmony/SDL_syspower.c @@ -0,0 +1,46 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifndef SDL_POWER_DISABLED +#ifdef SDL_POWER_OPENHARMONY + +#include + +bool SDL_GetPowerInfo_OpenHarmony(SDL_PowerState *state, int *seconds, int *percent) +{ + *seconds = -1; + *percent = (int) OH_BatteryInfo_GetCapacity(); + + const BatteryInfo_BatteryPluggedType pluggedtype = OH_BatteryInfo_GetPluggedType(); + if (pluggedtype == PLUGGED_TYPE_NONE) { + *state = SDL_POWERSTATE_ON_BATTERY; + } else { + *state = (*percent < 100) ? SDL_POWERSTATE_CHARGING : SDL_POWERSTATE_CHARGED; + } + + return true; +} + +#endif // SDL_POWER_OPENHARMONY +#endif // SDL_POWER_DISABLED + diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c index 51516b6ef6..302abe8945 100644 --- a/src/render/SDL_render.c +++ b/src/render/SDL_render.c @@ -40,7 +40,7 @@ SDL_AddEventWatch to catch SDL_EVENT_WILL_ENTER_BACKGROUND events and stopped drawing themselves. Other platforms still draw, as the compositor can use it, and more importantly: drawing to render targets isn't lost. But I still think this should probably be removed at some point in the future. --ryan. */ -#if defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS) || defined(SDL_PLATFORM_ANDROID) +#if defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) #define DONT_DRAW_WHILE_HIDDEN 1 #else #define DONT_DRAW_WHILE_HIDDEN 0 diff --git a/src/thread/pthread/SDL_systhread.c b/src/thread/pthread/SDL_systhread.c index eb13d02490..13b3ad0e0a 100644 --- a/src/thread/pthread/SDL_systhread.c +++ b/src/thread/pthread/SDL_systhread.c @@ -40,7 +40,7 @@ #include "../../core/linux/SDL_dbus.h" #endif // SDL_PLATFORM_LINUX -#if (defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)) && defined(HAVE_DLOPEN) +#if (defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) || defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)) && defined(HAVE_DLOPEN) #include #ifndef RTLD_DEFAULT #define RTLD_DEFAULT NULL @@ -77,7 +77,7 @@ static void *RunThread(void *data) #if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)) && defined(HAVE_DLOPEN) static bool checked_setname = false; static int (*ppthread_setname_np)(const char *) = NULL; -#elif (defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID)) && defined(HAVE_DLOPEN) +#elif (defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)) && defined(HAVE_DLOPEN) static bool checked_setname = false; static int (*ppthread_setname_np)(pthread_t, const char *) = NULL; #endif @@ -88,12 +88,12 @@ bool SDL_SYS_CreateThread(SDL_Thread *thread, pthread_attr_t type; // do this here before any threads exist, so there's no race condition. -#if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID)) && defined(HAVE_DLOPEN) +#if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)) && defined(HAVE_DLOPEN) if (!checked_setname) { void *fn = dlsym(RTLD_DEFAULT, "pthread_setname_np"); #if defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) ppthread_setname_np = (int (*)(const char *))fn; -#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) +#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) ppthread_setname_np = (int (*)(pthread_t, const char *))fn; #endif checked_setname = true; @@ -129,12 +129,12 @@ void SDL_SYS_SetupThread(const char *name) #endif if (name) { -#if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID)) && defined(HAVE_DLOPEN) +#if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)) && defined(HAVE_DLOPEN) SDL_assert(checked_setname); if (ppthread_setname_np) { #if defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) ppthread_setname_np(name); -#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) +#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) if (ppthread_setname_np(pthread_self(), name) == ERANGE) { char namebuf[16]; // Limited to 16 char SDL_strlcpy(namebuf, name, sizeof(namebuf)); @@ -171,6 +171,9 @@ void SDL_SYS_SetupThread(const char *name) pthread_sigmask(SIG_BLOCK, &mask, NULL); #endif +// HarmonyOS defines this, but doesn't offer pthread_setcanceltype. +// !!! FIXME: but it looks like we shouldn't be using this on any platform! https://clang.llvm.org/extra/clang-tidy/checks/concurrency/thread-canceltype-asynchronous.html +#ifndef SDL_PLATFORM_OPENHARMONY #ifdef PTHREAD_CANCEL_ASYNCHRONOUS // Allow ourselves to be asynchronously cancelled { @@ -178,6 +181,7 @@ void SDL_SYS_SetupThread(const char *name) pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldstate); } #endif +#endif } SDL_ThreadID SDL_GetCurrentThreadID(void) diff --git a/src/video/SDL_egl.c b/src/video/SDL_egl.c index d5713ac55a..686931fe79 100644 --- a/src/video/SDL_egl.c +++ b/src/video/SDL_egl.c @@ -86,6 +86,10 @@ #define DEFAULT_OGL_ES_PVR "libGLES_CM.so" #define DEFAULT_OGL_ES "libGLESv1_CM.so" +#elif defined(SDL_VIDEO_DRIVER_OPENHARMONY) +#define DEFAULT_EGL "libEGL.so" +#define DEFAULT_OGL_ES2 "libGLESv3.so" + #elif defined(SDL_VIDEO_DRIVER_WINDOWS) // EGL AND OpenGL ES support via ANGLE #define DEFAULT_EGL "libEGL.dll" diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h index 76d924c25d..8b71f803fa 100644 --- a/src/video/SDL_sysvideo.h +++ b/src/video/SDL_sysvideo.h @@ -529,6 +529,7 @@ extern VideoBootStrap WINDOWS_bootstrap; extern VideoBootStrap HAIKU_bootstrap; extern VideoBootStrap UIKIT_bootstrap; extern VideoBootStrap Android_bootstrap; +extern VideoBootStrap OPENHARMONY_bootstrap; extern VideoBootStrap PS2_bootstrap; extern VideoBootStrap PSP_bootstrap; extern VideoBootStrap VITA_bootstrap; diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index 4c8611828d..890d3637a0 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -110,6 +110,9 @@ static VideoBootStrap *bootstrap[] = { #ifdef SDL_VIDEO_DRIVER_ANDROID &Android_bootstrap, #endif +#ifdef SDL_VIDEO_DRIVER_OPENHARMONY + &OPENHARMONY_bootstrap, +#endif #ifdef SDL_VIDEO_DRIVER_PS2 &PS2_bootstrap, #endif diff --git a/src/video/SDL_vulkan_internal.h b/src/video/SDL_vulkan_internal.h index 4a828b90df..e60f3aae69 100644 --- a/src/video/SDL_vulkan_internal.h +++ b/src/video/SDL_vulkan_internal.h @@ -27,6 +27,9 @@ #ifdef SDL_VIDEO_DRIVER_ANDROID #define VK_USE_PLATFORM_ANDROID_KHR #endif +#ifdef SDL_VIDEO_DRIVER_OPENHARMONY +#define VK_USE_PLATFORM_OHOS +#endif #ifdef SDL_VIDEO_DRIVER_COCOA #define VK_USE_PLATFORM_METAL_EXT #define VK_USE_PLATFORM_MACOS_MVK diff --git a/src/video/openharmony/SDL_openharmonyclipboard.c b/src/video/openharmony/SDL_openharmonyclipboard.c new file mode 100644 index 0000000000..5927d187e3 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyclipboard.c @@ -0,0 +1,220 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_OPENHARMONY + +#include +#include +#include +#include + +#include "SDL_openharmonyvideo.h" +#include "SDL_openharmonyclipboard.h" +#include "../../core/openharmony/SDL_openharmony.h" +#include "../../events/SDL_events_c.h" + +static void PasteboardNotifyCallback(void *userdata, Pasteboard_NotifyType type) +{ + SDL_assert(userdata != NULL); + SDL_VideoDevice *_this = (SDL_VideoDevice *) userdata; + SDL_VideoData *data = _this->internal; + + if (data->clipboard_set) { + data->clipboard_set = false; // this was us, ignore it. + return; + } + + OH_Pasteboard *pasteboard = (OH_Pasteboard *) data->oh_pasteboard; + SDL_assert(pasteboard != NULL); + + const char *mimetype = "text/plain"; + if (OH_Pasteboard_HasType(pasteboard, mimetype)) { + const size_t slen = SDL_strlen(mimetype); + const size_t allocationsize = (slen + 1) + (sizeof (char *) * 2); + char **new_mime_types = SDL_AllocateTemporaryMemory(allocationsize); + if (new_mime_types) { + char *ptr = (char *)(new_mime_types + 2); + SDL_strlcpy(ptr, mimetype, slen + 1); + new_mime_types[0] = ptr; + new_mime_types[1] = NULL; + SDL_SendClipboardUpdate(false, new_mime_types, 1); + } + } +} + +static void PasteboardFinalizeCallback(void *userdata) +{ + // do nothing. +} + +void OPENHARMONY_InitClipboard(SDL_VideoDevice *_this) +{ + SDL_VideoData *data = _this->internal; + + data->clipboard_set = false; + + OH_Pasteboard *pasteboard = OH_Pasteboard_Create(); + data->oh_pasteboard = pasteboard; + + OH_PasteboardObserver *observer = OH_PasteboardObserver_Create(); + data->oh_pasteboard_observer = observer; + if (observer) { + OH_PasteboardObserver_SetData(observer, _this, PasteboardNotifyCallback, PasteboardFinalizeCallback); + } + + if (pasteboard && observer) { + OH_Pasteboard_Subscribe(pasteboard, NOTIFY_LOCAL_DATA_CHANGE, observer); + } +} + +void OPENHARMONY_QuitClipboard(SDL_VideoDevice *_this) +{ + SDL_VideoData *data = _this->internal; + + OH_PasteboardObserver *observer = (OH_PasteboardObserver *) data->oh_pasteboard_observer; + OH_Pasteboard *pasteboard = (OH_Pasteboard *) data->oh_pasteboard; + + if (observer && pasteboard) { + OH_Pasteboard_Unsubscribe(pasteboard, NOTIFY_LOCAL_DATA_CHANGE, observer); + } + + if (observer) { + OH_PasteboardObserver_Destroy(observer); + data->oh_pasteboard_observer = NULL; + } + + if (pasteboard) { + OH_Pasteboard_Destroy(pasteboard); + data->oh_pasteboard = NULL; + } +} + +const char *const *OPENHARMONY_GetTextMimeTypes(SDL_VideoDevice *_this, size_t *num_mime_types) +{ + static const char *const text_mime_types[] = { "text/plain" }; + *num_mime_types = SDL_arraysize(text_mime_types); + return text_mime_types; +} + +bool OPENHARMONY_SetClipboardText(SDL_VideoDevice *_this, const char *text) +{ + SDL_VideoData *data = _this->internal; + OH_Pasteboard *pasteboard = (OH_Pasteboard *) data->oh_pasteboard; + if (!pasteboard) { + return SDL_SetError("Pasteboard not initialized"); + } + + OH_UdsPlainText *udspt = OH_UdsPlainText_Create(); + if (!udspt) { + return SDL_SetError("Could not create OH_UdsPlainText instance"); + } + OH_UdsPlainText_SetContent(udspt, text); + + OH_UdmfRecord *udmfrec = OH_UdmfRecord_Create(); + if (!udmfrec) { + OH_UdsPlainText_Destroy(udspt); + return SDL_SetError("Could not create OH_UdmfRecord instance"); + } + OH_UdmfRecord_AddPlainText(udmfrec, udspt); + + OH_UdmfData *udmfdata = OH_UdmfData_Create(); + if (!udmfdata) { + OH_UdmfRecord_Destroy(udmfrec); + OH_UdsPlainText_Destroy(udspt); + return SDL_SetError("Could not create OH_UdmfData instance"); + } + OH_UdmfData_AddRecord(udmfdata, udmfrec); + + data->clipboard_set = true; + const PASTEBOARD_ErrCode rc = OH_Pasteboard_SetData(pasteboard, udmfdata); + + OH_UdmfData_Destroy(udmfdata); + OH_UdmfRecord_Destroy(udmfrec); + OH_UdsPlainText_Destroy(udspt); + + if (rc != ERR_OK) { + data->clipboard_set = true; + return SDL_SetError("OH_Pasteboard_SetData failed: %d", (int) rc); + } + return true; +} + +char *OPENHARMONY_GetClipboardText(SDL_VideoDevice *_this) +{ + SDL_VideoData *data = _this->internal; + OH_Pasteboard *pasteboard = (OH_Pasteboard *) data->oh_pasteboard; + if (!pasteboard) { + SDL_SetError("Pasteboard not initialized"); + return NULL; + } + + char *retval = NULL; + if (OH_Pasteboard_HasType(pasteboard, "text/plain")) { + int rc = (int) ERR_OK; + OH_UdmfData *udmfdata = OH_Pasteboard_GetData(pasteboard, &rc); + if (!udmfdata) { + SDL_SetError("Could not create OH_UdmfData instance %d", rc); + return NULL; + } + + OH_UdmfRecord *udmfrec = OH_UdmfData_GetRecord(udmfdata, 0); // 0 == first record in udmfdata + if (!udmfrec) { + OH_UdmfData_Destroy(udmfdata); + SDL_SetError("Could not create OH_UdmfRecord instance"); + return NULL; + } + + OH_UdsPlainText *udspt = OH_UdsPlainText_Create(); + if (!udspt) { + OH_UdmfRecord_Destroy(udmfrec); + OH_UdmfData_Destroy(udmfdata); + SDL_SetError("Could not create OH_UdsPlainText instance"); + return NULL; + } + + OH_UdmfRecord_GetPlainText(udmfrec, udspt); + const char *utf8 = OH_UdsPlainText_GetContent(udspt); + if (!utf8) { + SDL_SetError("Could not get plaintext content"); + } else { + retval = SDL_strdup(utf8); + } + + OH_UdsPlainText_Destroy(udspt); + OH_UdmfRecord_Destroy(udmfrec); + OH_UdmfData_Destroy(udmfdata); + } + + return retval; +} + +bool OPENHARMONY_HasClipboardText(SDL_VideoDevice *_this) +{ + SDL_VideoData *data = _this->internal; + OH_Pasteboard *pasteboard = (OH_Pasteboard *) data->oh_pasteboard; + if (!pasteboard) { + return SDL_SetError("Pasteboard not initialized"); + } + return OH_Pasteboard_HasType(pasteboard, "text/plain"); +} + +#endif // SDL_VIDEO_DRIVER_OPENHARMONY diff --git a/src/video/openharmony/SDL_openharmonyclipboard.h b/src/video/openharmony/SDL_openharmonyclipboard.h new file mode 100644 index 0000000000..00d9fefeee --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyclipboard.h @@ -0,0 +1,34 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_openharmonyclipboard_h_ +#define SDL_openharmonyclipboard_h_ + +extern void OPENHARMONY_InitClipboard(SDL_VideoDevice *_this); +extern void OPENHARMONY_QuitClipboard(SDL_VideoDevice *_this); + +extern const char *const *OPENHARMONY_GetTextMimeTypes(SDL_VideoDevice *_this, size_t *num_mime_types); +extern bool OPENHARMONY_SetClipboardText(SDL_VideoDevice *_this, const char *text); +extern char *OPENHARMONY_GetClipboardText(SDL_VideoDevice *_this); +extern bool OPENHARMONY_HasClipboardText(SDL_VideoDevice *_this); + +#endif // SDL_openharmonyclipboard_h_ diff --git a/src/video/openharmony/SDL_openharmonyevents.c b/src/video/openharmony/SDL_openharmonyevents.c new file mode 100644 index 0000000000..311a516b07 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyevents.c @@ -0,0 +1,574 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_OPENHARMONY + +// !!! FIXME: these are defined as "const uint32_t VARNAME = VALUE;" in native_interface_xcomponent.h, which becomes a global variable in _our_ C code! Maybe C++ handles this differently...? +#define OH_XCOMPONENT_ID_LEN_MAX sdl_ohosevents_OH_XCOMPONENT_ID_LEN_MAX +#define OH_MAX_TOUCH_POINTS_NUMBER sdl_ohosevents_OH_MAX_TOUCH_POINTS_NUMBER +#include + +#include "SDL_openharmonyevents.h" +//#include "SDL_openharmonykeyboard.h" +#include "SDL_openharmonywindow.h" +#include "../SDL_sysvideo.h" +#include "../../events/SDL_events_c.h" + +static SDL_Scancode MapOHKeycodeToSDLScancode(const OH_NativeXComponent_KeyCode ohcode) +{ + // !!! FIXME: this needs a serious looking-over. + switch (ohcode) { + #define KEYMAP(oh,sdl) case KEY_##oh: return SDL_SCANCODE_##sdl + KEYMAP(UNKNOWN, UNKNOWN); + //case KEY_FN = 0, + KEYMAP(HOME, AC_HOME); + KEYMAP(BACK, AC_BACK); + KEYMAP(MEDIA_PLAY_PAUSE, MEDIA_PLAY_PAUSE); + KEYMAP(MEDIA_STOP, MEDIA_STOP); + KEYMAP(MEDIA_NEXT, MEDIA_NEXT_TRACK); + KEYMAP(MEDIA_PREVIOUS, MEDIA_PREVIOUS_TRACK); + KEYMAP(MEDIA_REWIND, MEDIA_REWIND); + KEYMAP(MEDIA_FAST_FORWARD, MEDIA_FAST_FORWARD); + KEYMAP(VOLUME_UP, VOLUMEUP); + KEYMAP(VOLUME_DOWN, VOLUMEDOWN); + KEYMAP(POWER, POWER); + //KEYMAP(CAMERA, CAMERA); + KEYMAP(VOLUME_MUTE, MUTE); + KEYMAP(MUTE, MUTE); + //KEYMAP(BRIGHTNESS_UP = 40, + //KEYMAP(BRIGHTNESS_DOWN = 41, + KEYMAP(0, 0); + KEYMAP(1, 1); + KEYMAP(2, 2); + KEYMAP(3, 3); + KEYMAP(4, 4); + KEYMAP(5, 5); + KEYMAP(6, 6); + KEYMAP(7, 7); + KEYMAP(8, 8); + KEYMAP(9, 9); + //KEYMAP(STAR = 2010, + //KEYMAP(POUND = 2011, + KEYMAP(DPAD_UP, UP); + KEYMAP(DPAD_DOWN, DOWN); + KEYMAP(DPAD_LEFT, LEFT); + KEYMAP(DPAD_RIGHT, RIGHT); + //KEYMAP(DPAD_CENTER = 2016, + KEYMAP(A, A); + KEYMAP(B, B); + KEYMAP(C, C); + KEYMAP(D, D); + KEYMAP(E, E); + KEYMAP(F, F); + KEYMAP(G, G); + KEYMAP(H, H); + KEYMAP(I, I); + KEYMAP(J, J); + KEYMAP(K, K); + KEYMAP(L, L); + KEYMAP(M, M); + KEYMAP(N, N); + KEYMAP(O, O); + KEYMAP(P, P); + KEYMAP(Q, Q); + KEYMAP(R, R); + KEYMAP(S, S); + KEYMAP(T, T); + KEYMAP(U, U); + KEYMAP(V, V); + KEYMAP(W, W); + KEYMAP(X, X); + KEYMAP(Y, Y); + KEYMAP(Z, Z); + KEYMAP(COMMA, COMMA); + KEYMAP(PERIOD, PERIOD); + KEYMAP(ALT_LEFT, LALT); + KEYMAP(ALT_RIGHT, RALT); + KEYMAP(SHIFT_LEFT, LSHIFT); + KEYMAP(SHIFT_RIGHT, RSHIFT); + KEYMAP(TAB, TAB); + KEYMAP(SPACE, SPACE); + //KEYMAP(SYM = 2051, + //KEYMAP(EXPLORER = 2052, + //KEYMAP(ENVELOPE = 2053, + KEYMAP(ENTER, RETURN); + KEYMAP(DEL, DELETE); + KEYMAP(GRAVE, GRAVE); + KEYMAP(MINUS, MINUS); + KEYMAP(EQUALS, EQUALS); + KEYMAP(LEFT_BRACKET, LEFTBRACKET); + KEYMAP(RIGHT_BRACKET, RIGHTBRACKET); + KEYMAP(BACKSLASH, BACKSLASH); + KEYMAP(SEMICOLON, SEMICOLON); + KEYMAP(APOSTROPHE, APOSTROPHE); + KEYMAP(SLASH, SLASH); + KEYMAP(AT, KP_AT); + KEYMAP(PLUS, KP_PLUS); + KEYMAP(MENU, MENU); + KEYMAP(PAGE_UP, PAGEUP); + KEYMAP(PAGE_DOWN, PAGEDOWN); + KEYMAP(ESCAPE, ESCAPE); + //KEYMAP(FORWARD_DEL = 2071, + KEYMAP(CTRL_LEFT, LCTRL); + KEYMAP(CTRL_RIGHT, RCTRL); + KEYMAP(CAPS_LOCK, CAPSLOCK); + KEYMAP(SCROLL_LOCK, SCROLLLOCK); + KEYMAP(META_LEFT, LGUI); + KEYMAP(META_RIGHT, RGUI); + //KEYMAP(FUNCTION = 2078, + KEYMAP(SYSRQ, SYSREQ); + //KEYMAP(BREAK = 2080, + KEYMAP(MOVE_HOME, HOME); + KEYMAP(MOVE_END, END); + KEYMAP(INSERT, INSERT); + KEYMAP(FORWARD, AC_FORWARD); + KEYMAP(MEDIA_PLAY, MEDIA_PLAY); + KEYMAP(MEDIA_PAUSE, MEDIA_PAUSE); + //KEYMAP(MEDIA_CLOSE = 2087, + KEYMAP(MEDIA_EJECT, MEDIA_EJECT); + KEYMAP(MEDIA_RECORD, MEDIA_RECORD); + KEYMAP(F1, F1); + KEYMAP(F2, F2); + KEYMAP(F3, F3); + KEYMAP(F4, F4); + KEYMAP(F5, F5); + KEYMAP(F6, F6); + KEYMAP(F7, F7); + KEYMAP(F8, F8); + KEYMAP(F9, F9); + KEYMAP(F10, F10); + KEYMAP(F11, F11); + KEYMAP(F12, F12); + KEYMAP(NUM_LOCK, NUMLOCKCLEAR); + KEYMAP(NUMPAD_0, KP_0); + KEYMAP(NUMPAD_1, KP_1); + KEYMAP(NUMPAD_2, KP_2); + KEYMAP(NUMPAD_3, KP_3); + KEYMAP(NUMPAD_4, KP_4); + KEYMAP(NUMPAD_5, KP_5); + KEYMAP(NUMPAD_6, KP_6); + KEYMAP(NUMPAD_7, KP_7); + KEYMAP(NUMPAD_8, KP_8); + KEYMAP(NUMPAD_9, KP_9); + KEYMAP(NUMPAD_DIVIDE, KP_DIVIDE); + KEYMAP(NUMPAD_MULTIPLY, KP_MULTIPLY); + KEYMAP(NUMPAD_SUBTRACT, KP_MINUS); + KEYMAP(NUMPAD_ADD, KP_PLUS); + KEYMAP(NUMPAD_DOT, KP_PERIOD); + //KEYMAP(NUMPAD_COMMA = 2118, + KEYMAP(NUMPAD_ENTER, KP_ENTER); + //KEYMAP(NUMPAD_EQUALS = 2120, + KEYMAP(NUMPAD_LEFT_PAREN, KP_LEFTPAREN); + KEYMAP(NUMPAD_RIGHT_PAREN, KP_RIGHTPAREN); + //KEYMAP(VIRTUAL_MULTITASK = 2210, + KEYMAP(SLEEP, SLEEP); + KEYMAP(ZENKAKU_HANKAKU, LANG5); + //KEYMAP(102ND = 2602, + //KEYMAP(RO = 2603, + KEYMAP(KATAKANA, LANG3); + KEYMAP(HIRAGANA, LANG4); + //KEYMAP(HENKAN = 2606, + //KEYMAP(KATAKANA_HIRAGANA = 2607, + //KEYMAP(MUHENKAN = 2608, + //KEYMAP(LINEFEED = 2609, + //KEYMAP(MACRO = 2610, + KEYMAP(NUMPAD_PLUSMINUS, KP_PLUSMINUS); + //KEYMAP(SCALE = 2612, + //KEYMAP(HANGUEL = 2613, + //KEYMAP(HANJA = 2614, + //KEYMAP(YEN = 2615, + KEYMAP(STOP, STOP); + KEYMAP(AGAIN, AGAIN); + KEYMAP(PROPS, AC_PROPERTIES); // I think....? + KEYMAP(UNDO, UNDO); + KEYMAP(COPY, COPY); + KEYMAP(OPEN, AC_OPEN); /// I think...? + KEYMAP(PASTE, PASTE); + KEYMAP(FIND, FIND); + KEYMAP(CUT, CUT); + KEYMAP(HELP, HELP); + //KEYMAP(CALC = 2626, + //KEYMAP(FILE = 2627, + KEYMAP(BOOKMARKS, AC_BOOKMARKS); + KEYMAP(NEXT, MEDIA_NEXT_TRACK); + KEYMAP(PLAYPAUSE, MEDIA_PLAY_PAUSE); + KEYMAP(PREVIOUS, MEDIA_PREVIOUS_TRACK); + KEYMAP(STOPCD, MEDIA_STOP); + //KEYMAP(CONFIG = 2634, + KEYMAP(REFRESH, AC_REFRESH); + KEYMAP(EXIT, AC_EXIT); + //KEYMAP(EDIT = 2637, + //KEYMAP(SCROLLUP = 2638, + //KEYMAP(SCROLLDOWN = 2639, + KEYMAP(NEW, AC_NEW); + KEYMAP(REDO, AGAIN); + KEYMAP(CLOSE, AC_CLOSE); + KEYMAP(PLAY, MEDIA_PLAY); + //KEYMAP(BASSBOOST = 2644, + KEYMAP(PRINT, PRINTSCREEN); + //KEYMAP(CHAT = 2646, + //KEYMAP(FINANCE = 2647, + KEYMAP(CANCEL, CANCEL); + //KEYMAP(KBDILLUM_TOGGLE = 2649, + //KEYMAP(KBDILLUM_DOWN = 2650, + //KEYMAP(KBDILLUM_UP = 2651, + //KEYMAP(SEND = 2652, + //KEYMAP(REPLY = 2653, + //KEYMAP(FORWARDMAIL = 2654, + KEYMAP(SAVE, AC_SAVE); + //KEYMAP(DOCUMENTS = 2656, + //KEYMAP(VIDEO_NEXT = 2657, + //KEYMAP(VIDEO_PREV = 2658, + //KEYMAP(BRIGHTNESS_CYCLE = 2659, + //KEYMAP(BRIGHTNESS_ZERO = 2660, + //KEYMAP(DISPLAY_OFF = 2661, + //KEYMAP(BTN_MISC = 2662, + //KEYMAP(GOTO = 2663, + //KEYMAP(INFO = 2664, + //KEYMAP(PROGRAM = 2665, + //KEYMAP(PVR = 2666, + //KEYMAP(SUBTITLE = 2667, + //KEYMAP(FULL_SCREEN = 2668, + //KEYMAP(KEYBOARD = 2669, + //KEYMAP(ASPECT_RATIO = 2670, + //KEYMAP(PC = 2671, + //KEYMAP(TV = 2672, + //KEYMAP(TV2 = 2673, + //KEYMAP(VCR = 2674, + //KEYMAP(VCR2 = 2675, + //KEYMAP(SAT = 2676, + //KEYMAP(CD = 2677, + //KEYMAP(TAPE = 2678, + //KEYMAP(TUNER = 2679, + //KEYMAP(PLAYER = 2680, + //KEYMAP(DVD = 2681, + //KEYMAP(AUDIO = 2682, + //KEYMAP(VIDEO = 2683, + //KEYMAP(MEMO = 2684, + //KEYMAP(CALENDAR = 2685, + //KEYMAP(RED = 2686, + //KEYMAP(GREEN = 2687, + //KEYMAP(YELLOW = 2688, + //KEYMAP(BLUE = 2689, + //KEYMAP(CHANNELUP = 2690, + //KEYMAP(CHANNELDOWN = 2691, + //KEYMAP(LAST = 2692, + //KEYMAP(RESTART = 2693, + //KEYMAP(SLOW = 2694, + //KEYMAP(SHUFFLE = 2695, + //KEYMAP(VIDEOPHONE = 2696, + //KEYMAP(GAMES = 2697, + //KEYMAP(ZOOMIN = 2698, + //KEYMAP(ZOOMOUT = 2699, + //KEYMAP(ZOOMRESET = 2700, + //KEYMAP(WORDPROCESSOR = 2701, + //KEYMAP(EDITOR = 2702, + //KEYMAP(SPREADSHEET = 2703, + //KEYMAP(GRAPHICSEDITOR = 2704, + //KEYMAP(PRESENTATION = 2705, + //KEYMAP(DATABASE = 2706, + //KEYMAP(NEWS = 2707, + //KEYMAP(VOICEMAIL = 2708, + //KEYMAP(ADDRESSBOOK = 2709, + //KEYMAP(MESSENGER = 2710, + //KEYMAP(BRIGHTNESS_TOGGLE = 2711, + //KEYMAP(SPELLCHECK = 2712, + //KEYMAP(COFFEE = 2713, + //KEYMAP(MEDIA_REPEAT = 2714, + //KEYMAP(IMAGES = 2715, + //KEYMAP(BUTTONCONFIG = 2716, + //KEYMAP(TASKMANAGER = 2717, + //KEYMAP(JOURNAL = 2718, + //KEYMAP(CONTROLPANEL = 2719, + //KEYMAP(APPSELECT = 2720, + //KEYMAP(SCREENSAVER = 2721, + //KEYMAP(ASSISTANT = 2722, + //KEYMAP(KBD_LAYOUT_NEXT = 2723, + //KEYMAP(BRIGHTNESS_MIN = 2724, + //KEYMAP(BRIGHTNESS_MAX = 2725, + //KEYMAP(KBDINPUTASSIST_PREV = 2726, + //KEYMAP(KBDINPUTASSIST_NEXT = 2727, + //KEYMAP(KBDINPUTASSIST_PREVGROUP = 2728, + //KEYMAP(KBDINPUTASSIST_NEXTGROUP = 2729, + //KEYMAP(KBDINPUTASSIST_ACCEPT = 2730, + //KEYMAP(KBDINPUTASSIST_CANCEL = 2731, + //KEYMAP(FRONT = 2800, + //KEYMAP(SETUP = 2801, + //KEYMAP(WAKEUP = 2802, + //KEYMAP(SENDFILE = 2803, + //KEYMAP(DELETEFILE = 2804, + //KEYMAP(XFER = 2805, + //KEYMAP(PROG1 = 2806, + //KEYMAP(PROG2 = 2807, + //KEYMAP(MSDOS = 2808, + //KEYMAP(SCREENLOCK = 2809, + //KEYMAP(DIRECTION_ROTATE_DISPLAY = 2810, + //KEYMAP(CYCLEWINDOWS = 2811, + //KEYMAP(COMPUTER = 2812, + //KEYMAP(EJECTCLOSECD = 2813, + //KEYMAP(ISO = 2814, + //KEYMAP(MOVE = 2815, + KEYMAP(F13, F13); + KEYMAP(F14, F14); + KEYMAP(F15, F15); + KEYMAP(F16, F16); + KEYMAP(F17, F17); + KEYMAP(F18, F18); + KEYMAP(F19, F19); + KEYMAP(F20, F20); + KEYMAP(F21, F21); + KEYMAP(F22, F22); + KEYMAP(F23, F23); + KEYMAP(F24, F24); + //KEYMAP(PROG3 = 2828, + //KEYMAP(PROG4 = 2829, + //KEYMAP(DASHBOARD = 2830, + //KEYMAP(SUSPEND = 2831, + //KEYMAP(HP = 2832, + //KEYMAP(SOUND = 2833, + //KEYMAP(QUESTION = 2834, + //KEYMAP(CONNECT = 2836, + //KEYMAP(SPORT = 2837, + //KEYMAP(SHOP = 2838, + //KEYMAP(ALTERASE = 2839, + //KEYMAP(SWITCHVIDEOMODE = 2841, + //KEYMAP(BATTERY = 2842, + //KEYMAP(BLUETOOTH = 2843, + //KEYMAP(WLAN = 2844, + //KEYMAP(UWB = 2845, + //KEYMAP(WWAN_WIMAX = 2846, + //KEYMAP(RFKILL = 2847, + //KEYMAP(CHANNEL = 3001, + //KEYMAP(BTN_0 = 3100, + //KEYMAP(BTN_1 = 3101, + //KEYMAP(BTN_2 = 3102, + //KEYMAP(BTN_3 = 3103, + //KEYMAP(BTN_4 = 3104, + //KEYMAP(BTN_5 = 3105, + //KEYMAP(BTN_6 = 3106, + //KEYMAP(BTN_7 = 3107, + //KEYMAP(BTN_8 = 3108, + //KEYMAP(BTN_9 = 3109, + #undef KEYMAP + default: break; + } + + return SDL_SCANCODE_UNKNOWN; +} + +void SDL_OpenHarmonyDispatchTouchEvent(void *component, void *window) +{ + OH_NativeXComponent *xcomponent = (OH_NativeXComponent *) component; + OH_NativeXComponent_TouchEvent event; + if (OH_NativeXComponent_GetTouchEvent(xcomponent, window, &event) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + return; // oh well. + } + +#if 0 + SDL_Log("TOUCH EVENT! id=%d screenX=%f screenY=%f x=%f y=%f type=%d size=%f force=%f devid=%lld timestamp=%lld numpoints=%d", (int) event.id, event.screenX, event.screenY, event.x, event.y, (int) event.type, (float) event.size, event.force, (long long) event.deviceId, (long long) event.timeStamp, (int) event.numPoints); + for (int i = 0; i < event.numPoints; i++) { + SDL_Log("TOUCH POINT #%d: id=%d screenX=%f screenY=%f x=%f y=%f type=%d size=%f force=%f timestamp=%lld ispressed=%s", i, (int) event.touchPoints[i].id, event.touchPoints[i].screenX, event.touchPoints[i].screenY, event.touchPoints[i].x, event.touchPoints[i].y, (int) event.touchPoints[i].type, (float) event.touchPoints[i].size, event.touchPoints[i].force, (long long) event.touchPoints[i].timeStamp, event.touchPoints[i].isPressed ? "true" : "false"); + } +#endif + + SDL_EventType sdleventtype = SDL_EVENT_FIRST; + switch (event.type) { + #define EVTYPEMAP(ohevent, sdlevent) case OH_NATIVEXCOMPONENT_##ohevent: sdleventtype = SDL_EVENT_FINGER_##sdlevent; break + EVTYPEMAP(DOWN, DOWN); + EVTYPEMAP(UP, UP); + EVTYPEMAP(MOVE, MOTION); + EVTYPEMAP(CANCEL, CANCELED); + default: break; + #undef EVTYPEMAP + } + + if (sdleventtype == SDL_EVENT_FIRST) { // FIRST == unknown event type. + return; // nothing to do. + } + + int touchidx = 0; + while (touchidx < event.numPoints) { + if (event.touchPoints[touchidx].id == event.id) { + break; + } + touchidx++; + } + + if (touchidx >= event.numPoints) { + return; // uh...we don't have this touch...? + } + + OH_NativeXComponent_TouchPointToolType tooltype; + if (OH_NativeXComponent_GetTouchPointToolType(xcomponent, touchidx, &tooltype) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + return; // bad bad bad + } + + const float normalized_x = OPENHARMONY_Window->w ? (event.x / ((float) OPENHARMONY_Window->w)) : 0.0f; + const float normalized_y = OPENHARMONY_Window->h ? (event.y / ((float) OPENHARMONY_Window->h)) : 0.0f; + + if (tooltype == OH_NATIVEXCOMPONENT_TOOL_TYPE_FINGER) { + const SDL_TouchID touchid = (SDL_TouchID)(((uintptr_t)event.deviceId) + 1); + SDL_AddTouch(touchid, SDL_TOUCH_DEVICE_DIRECT, NULL); + if (sdleventtype == SDL_EVENT_FINGER_MOTION) { + SDL_SendTouchMotion((Uint64) event.timeStamp, touchid, (SDL_FingerID)(((uintptr_t)event.id)+1), OPENHARMONY_Window, normalized_x, normalized_y, event.force); + } else { + SDL_SendTouch((Uint64) event.timeStamp, touchid, (SDL_FingerID)(((uintptr_t)event.id)+1), OPENHARMONY_Window, sdleventtype, normalized_x, normalized_y, event.force); + } + return; + } + +#if 0 // !!! FIXME: see if Pen events are useful here. + switch (tooltype) { + /** Indicates invalid tool type. */ + OH_NATIVEXCOMPONENT_TOOL_TYPE_UNKNOWN = 0, + /** Indicates a finger. */ + OH_NATIVEXCOMPONENT_TOOL_TYPE_FINGER, + /** Indicates a stylus. */ + OH_NATIVEXCOMPONENT_TOOL_TYPE_PEN, + /** Indicates an eraser. */ + OH_NATIVEXCOMPONENT_TOOL_TYPE_RUBBER, + /** Indicates a brush. */ + OH_NATIVEXCOMPONENT_TOOL_TYPE_BRUSH, + /** Indicates a pencil. */ + OH_NATIVEXCOMPONENT_TOOL_TYPE_PENCIL, + /** Indicates a brush. */ + OH_NATIVEXCOMPONENT_TOOL_TYPE_AIRBRUSH, + /** Indicates a mouse. */ + OH_NATIVEXCOMPONENT_TOOL_TYPE_MOUSE, + /** Indicates a lens. */ + OH_NATIVEXCOMPONENT_TOOL_TYPE_LENS, + } +#endif +} + +void SDL_OpenHarmonyDispatchMouseEvent(void *component, void *window) +{ + OH_NativeXComponent *xcomponent = (OH_NativeXComponent *) component; + OH_NativeXComponent_MouseEvent event; + if (OH_NativeXComponent_GetMouseEvent(xcomponent, window, &event) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + return; // oh well. + } + +#if 1 + SDL_Log("MOUSE EVENT! screenX=%f screenY=%f x=%f y=%f timestamp=%lld action=%d button=0x%X", event.screenX, event.screenY, event.x, event.y, (long long) event.timestamp, (int) event.action, (unsigned int) event.button); +#endif + + const SDL_MouseID mouseid = SDL_DEFAULT_MOUSE_ID; // !!! FIXME: should this be SDL_GLOBAL_MOUSE_ID or SDL_DEFAULT_MOUSE_ID? + bool down = false; + switch (event.action) { + case OH_NATIVEXCOMPONENT_MOUSE_PRESS: + down = true; + SDL_FALLTHROUGH; + case OH_NATIVEXCOMPONENT_MOUSE_RELEASE: + // these are bitmasks, so while I _assume_ you won't see more than one button per event, check them all separately, just in case. + #define CHECK_BUTTON(ohos, sdl) if (event.button & OH_NATIVEXCOMPONENT_##ohos##_BUTTON) { SDL_SendMouseButton((Uint64) event.timestamp, OPENHARMONY_Window, mouseid, SDL_BUTTON_##sdl, down); } + CHECK_BUTTON(LEFT, LEFT); + CHECK_BUTTON(RIGHT, RIGHT); + CHECK_BUTTON(MIDDLE, MIDDLE); + CHECK_BUTTON(BACK, X1); + CHECK_BUTTON(FORWARD, X2); + #undef CHECK_BUTTON + break; + + case OH_NATIVEXCOMPONENT_MOUSE_MOVE: + SDL_SendMouseMotion((Uint64) event.timestamp, OPENHARMONY_Window, mouseid, false, event.x, event.y); + break; + + default: break; // nothing to do? + } +} + +void SDL_OpenHarmonyDispatchUIInputEvent(void *vcomponent, void *vevent, int32_t vtype) +{ +// OH_NativeXComponent *component = (OH_NativeXComponent *) vcomponent; + ArkUI_UIInputEvent *event = (ArkUI_UIInputEvent *) vevent; + const ArkUI_UIInputEvent_Type type = (const ArkUI_UIInputEvent_Type) vtype; + + if (type != ARKUI_UIINPUTEVENT_TYPE_AXIS) { + return; // this is all we handle here for now. Maybe more in the future! + } else if (OH_ArkUI_UIInputEvent_GetSourceType(event) != UI_INPUT_EVENT_SOURCE_TYPE_MOUSE) { + return; // skip two-finger scrolling (...for now...?) + } + + const SDL_MouseID mouseid = SDL_DEFAULT_MOUSE_ID; // !!! FIXME: should this be SDL_GLOBAL_MOUSE_ID or SDL_DEFAULT_MOUSE_ID? + const double vertical = OH_ArkUI_AxisEvent_GetVerticalAxisValue(event); + if (vertical != 0.0) { + // !!! FIXME: OpenHarmony has a system setting for "natural" scrolling we can use for SDL_MouseWheelDirection, but I + // !!! FIXME: don't know how to access it right now. + // a single "click" of the wheel on my mouse is 45 degrees (360 / 8 click positions), so let's assume that's normal (and what Windows would do with WHEEL_DELTA, which is a different value with the same concept). + SDL_SendMouseWheel((Uint64) OH_ArkUI_UIInputEvent_GetEventTime(event), OPENHARMONY_Window, mouseid, 0.0f, -(vertical / 45.0f), SDL_MOUSEWHEEL_NORMAL); + } + + // !!! FIXME: this is in pixels, not degrees, and I'm not sure how to convert that yet. + // !!! FIXME: It's possible they don't support horizontal mouse wheels, and this is only for two-finger scrolling. + #if 0 + const double horizontal = OH_ArkUI_AxisEvent_GetHorizontalAxisValue(event); + if (horizontal != 0.0) { + } + #endif +} + +void SDL_OpenHarmonyDispatchKeyEvent(void *component, void *window) +{ + OH_NativeXComponent *xcomponent = (OH_NativeXComponent *) component; + OH_NativeXComponent_KeyEvent *event = NULL; + if (OH_NativeXComponent_GetKeyEvent(xcomponent, &event) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + return; // oh well. + } + + OH_NativeXComponent_KeyCode keycode = KEY_UNKNOWN; + OH_NativeXComponent_GetKeyEventCode(event, &keycode); + const SDL_Scancode scancode = MapOHKeycodeToSDLScancode(keycode); + if (scancode == SDL_SCANCODE_UNKNOWN) { + return; // oh well. + } + + OH_NativeXComponent_KeyAction action = OH_NATIVEXCOMPONENT_KEY_ACTION_UNKNOWN; + OH_NativeXComponent_GetKeyEventAction(event, &action); + bool down; + if (action == OH_NATIVEXCOMPONENT_KEY_ACTION_DOWN) { + down = true; + } else if (action == OH_NATIVEXCOMPONENT_KEY_ACTION_UP) { + down = false; + } else { + return; // oh well. + } + + int64_t timestamp = 0; + OH_NativeXComponent_GetKeyEventTimestamp(event, ×tamp); + + SDL_SendKeyboardKey((Uint64) timestamp, SDL_DEFAULT_KEYBOARD_ID, keycode, scancode, down); +} + +void OPENHARMONY_InitEvents(void) +{ +} + +void OPENHARMONY_PumpEvents(SDL_VideoDevice *_this) +{ +} + +void OPENHARMONY_QuitEvents(void) +{ +} + +#endif // SDL_VIDEO_DRIVER_OPENHARMONY diff --git a/src/video/openharmony/SDL_openharmonyevents.h b/src/video/openharmony/SDL_openharmonyevents.h new file mode 100644 index 0000000000..ee0f9d1bd3 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyevents.h @@ -0,0 +1,33 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "../SDL_sysvideo.h" + +// src/core/openharmony/SDL_openharmony.c calls these when it gets events from the system. +extern void SDL_OpenHarmonyDispatchTouchEvent(void *component, void *window); +extern void SDL_OpenHarmonyDispatchMouseEvent(void *component, void *window); +extern void SDL_OpenHarmonyDispatchKeyEvent(void *component, void *window); +extern void SDL_OpenHarmonyDispatchUIInputEvent(void *component, void *event, int32_t type); + +extern void OPENHARMONY_InitEvents(void); +extern void OPENHARMONY_PumpEvents(SDL_VideoDevice *_this); +extern void OPENHARMONY_QuitEvents(void); diff --git a/src/video/openharmony/SDL_openharmonykeyboard.c b/src/video/openharmony/SDL_openharmonykeyboard.c new file mode 100644 index 0000000000..c26a9f130d --- /dev/null +++ b/src/video/openharmony/SDL_openharmonykeyboard.c @@ -0,0 +1,93 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_OPENHARMONY + +#include "../../events/SDL_events_c.h" + +#include "SDL_openharmonykeyboard.h" + +#include "../../core/openharmony/SDL_openharmony.h" + +bool OPENHARMONY_HasScreenKeyboardSupport(SDL_VideoDevice *_this) +{ + return true; +} + +void OPENHARMONY_ShowScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props) +{ + int input_type = 0; + int cap_type = 0; + if (SDL_HasProperty(props, SDL_PROP_TEXTINPUT_OPENHARMONY_INPUTTYPE_NUMBER)) { + input_type = (int)SDL_GetNumberProperty(props, SDL_PROP_TEXTINPUT_OPENHARMONY_INPUTTYPE_NUMBER, 0); + } else { + switch (SDL_GetTextInputType(props)) { + #define INPUTTYPE(sdl,oh) case SDL_TEXTINPUT_TYPE_##sdl: input_type = oh; break + INPUTTYPE(TEXT, 0); + INPUTTYPE(TEXT_NAME, 0); + INPUTTYPE(TEXT_EMAIL, 5); + INPUTTYPE(TEXT_USERNAME, 10); + INPUTTYPE(TEXT_PASSWORD_HIDDEN, 7); // !!! FIXME: this is _also_ PASSWORD_VISIBLE, but I think this doesn't dictate rendering...? + INPUTTYPE(TEXT_PASSWORD_VISIBLE, 7); + INPUTTYPE(NUMBER_PASSWORD_HIDDEN, 8); // !!! FIXME: this is _also_ NUMBER_PASSWORD_VISIBLE, but I think this doesn't dictate rendering...? + INPUTTYPE(NUMBER_PASSWORD_VISIBLE, 8); + #undef INPUTTYPE + default: break; + } + + switch (SDL_GetTextInputCapitalization(props)) { + #define CAPTYPE(sdl,oh) case SDL_CAPITALIZE_##sdl: cap_type = oh; break + CAPTYPE(NONE, 0); + CAPTYPE(LETTERS, 3); + CAPTYPE(WORDS, 2); + CAPTYPE(SENTENCES, 1); + #undef CAPTYPE + default: break; + } + + #if 0 // !!! FIXME + if (SDL_GetTextInputAutocorrect(props)) { + input_type |= (TYPE_TEXT_FLAG_AUTO_CORRECT | TYPE_TEXT_FLAG_AUTO_COMPLETE); + } + #endif + + if (SDL_GetTextInputMultiline(props)) { + input_type = 1; // sorry, this only works with basic TEXT type. + } + } + SDL_OpenHarmonyShowScreenKeyboard(input_type, cap_type, &window->text_input_rect); +} + +void OPENHARMONY_HideScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window) +{ + SDL_OpenHarmonyHideScreenKeyboard(); +} + +void OPENHARMONY_RestoreScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window) +{ + if (_this->screen_keyboard_shown) { + OPENHARMONY_ShowScreenKeyboard(_this, window, window->text_input_props); + } +} + +#endif // SDL_VIDEO_DRIVER_OPENHARMONY + diff --git a/src/video/openharmony/SDL_openharmonykeyboard.h b/src/video/openharmony/SDL_openharmonykeyboard.h new file mode 100644 index 0000000000..3de748653a --- /dev/null +++ b/src/video/openharmony/SDL_openharmonykeyboard.h @@ -0,0 +1,29 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL_openharmonyvideo.h" + +extern bool OPENHARMONY_HasScreenKeyboardSupport(SDL_VideoDevice *_this); +extern void OPENHARMONY_ShowScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props); +extern void OPENHARMONY_HideScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window); +extern void OPENHARMONY_RestoreScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window); + diff --git a/src/video/openharmony/SDL_openharmonymouse.c b/src/video/openharmony/SDL_openharmonymouse.c new file mode 100644 index 0000000000..178d257409 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonymouse.c @@ -0,0 +1,76 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_OPENHARMONY + +#include "SDL_openharmonymouse.h" + +#include "../../events/SDL_mouse_c.h" + +#include "../../core/openharmony/SDL_openharmony.h" + +static bool OPENHARMONY_ShowCursor(SDL_Cursor *cursor) +{ +return false; +#if 0 + if (!cursor) { + cursor = OPENHARMONY_CreateEmptyCursor(); + } + if (cursor) { + SDL_CursorData *data = cursor->internal; + if (data->custom_cursor) { + if (!OPENHARMONY_JNI_SetCustomCursor(data->custom_cursor)) { + return SDL_Unsupported(); + } + } else { + if (!OPENHARMONY_JNI_SetSystemCursor(data->system_cursor)) { + return SDL_Unsupported(); + } + } + return true; + } else { + // SDL error set inside OPENHARMONY_CreateEmptyCursor() + return false; + } +#endif +} + +void OPENHARMONY_InitMouse(void) +{ + SDL_Mouse *mouse = SDL_GetMouse(); +//!!! FIXME: mouse->CreateCursor = OPENHARMONY_CreateCursor; +//!!! FIXME: mouse->CreateSystemCursor = OPENHARMONY_CreateSystemCursor; + mouse->ShowCursor = OPENHARMONY_ShowCursor; +//!!! FIXME: mouse->FreeCursor = OPENHARMONY_FreeCursor; +//!!! FIXME: mouse->SetRelativeMouseMode = OPENHARMONY_SetRelativeMouseMode; + +//!!! FIXME: SDL_SetDefaultCursor(OPENHARMONY_CreateDefaultCursor()); +} + +void OPENHARMONY_QuitMouse(void) +{ +//!!! FIXME: OPENHARMONY_DestroyEmptyCursor(); +} + +#endif + diff --git a/src/video/openharmony/SDL_openharmonymouse.h b/src/video/openharmony/SDL_openharmonymouse.h new file mode 100644 index 0000000000..f00266ffd2 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonymouse.h @@ -0,0 +1,30 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef SDL_openharmonymouse_h_ +#define SDL_openharmonymouse_h_ + +#include "SDL_openharmonyvideo.h" + +extern void OPENHARMONY_InitMouse(void); +extern void OPENHARMONY_QuitMouse(void); + +#endif // SDL_openharmonymouse_h_ diff --git a/src/video/openharmony/SDL_openharmonyopengl.c b/src/video/openharmony/SDL_openharmonyopengl.c new file mode 100644 index 0000000000..278c694232 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyopengl.c @@ -0,0 +1,79 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#if defined(SDL_VIDEO_DRIVER_OPENHARMONY) && defined(SDL_VIDEO_OPENGL_EGL) + +// OpenHarmony/HarmonyOS SDL video driver implementation + +#include "../SDL_egl_c.h" +#include "SDL_openharmonywindow.h" + +#include "SDL_openharmonyvideo.h" +#include "SDL_openharmonyevents.h" +#include "SDL_openharmonyopengl.h" +#include "../../core/openharmony/SDL_openharmony.h" + +bool OPENHARMONY_GLES_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context) +{ + if (window && context) { + return SDL_EGL_MakeCurrent(_this, window->internal->egl_surface, context); + } else { + return SDL_EGL_MakeCurrent(_this, NULL, NULL); + } +} + +SDL_GLContext OPENHARMONY_GLES_CreateContext(SDL_VideoDevice *_this, SDL_Window *window) +{ + SDL_GLContext result; + + // !!! FIXME: Android has locking here; maybe it isn't necessary for OpenHarmony? What happens if we're in a background thread? + //if (!OPENHARMONY_WaitActiveAndLockActivity()) { + // return NULL; + //} + + result = SDL_EGL_CreateContext(_this, window->internal->egl_surface); + + //OPENHARMONY_UnlockActivityMutex(); + + return result; +} + +bool OPENHARMONY_GLES_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + bool result; + + // !!! FIXME: Android has locking here; maybe it isn't necessary for OpenHarmony? What happens if we're in a background thread? + //OPENHARMONY_LockActivityMutex(); + + result = SDL_EGL_SwapBuffers(_this, window->internal->egl_surface); + + //OPENHARMONY_UnlockActivityMutex(); + + return result; +} + +bool OPENHARMONY_GLES_LoadLibrary(SDL_VideoDevice *_this, const char *path) +{ + return SDL_EGL_LoadLibrary(_this, path, EGL_DEFAULT_DISPLAY); +} + +#endif // SDL_VIDEO_DRIVER_OPENHARMONY diff --git a/src/video/openharmony/SDL_openharmonyopengl.h b/src/video/openharmony/SDL_openharmonyopengl.h new file mode 100644 index 0000000000..8bcea96693 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyopengl.h @@ -0,0 +1,31 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_openharmonyopengl_h_ +#define SDL_openharmonyopengl_h_ + +extern SDL_GLContext OPENHARMONY_GLES_CreateContext(SDL_VideoDevice *_this, SDL_Window *window); +extern bool OPENHARMONY_GLES_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context); +extern bool OPENHARMONY_GLES_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern bool OPENHARMONY_GLES_LoadLibrary(SDL_VideoDevice *_this, const char *path); + +#endif // SDL_openharmonyopengl_h_ diff --git a/src/video/openharmony/SDL_openharmonyvideo.c b/src/video/openharmony/SDL_openharmonyvideo.c new file mode 100644 index 0000000000..63ab3db5d9 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyvideo.c @@ -0,0 +1,280 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_OPENHARMONY + +// HarmonyOS/OpenHarmony SDL video driver implementation + +#include + +// !!! FIXME: these are defined as "const uint32_t VARNAME = VALUE;" in native_interface_xcomponent.h, which becomes a global variable in _our_ C code! Maybe C++ handles this differently...? +#define OH_XCOMPONENT_ID_LEN_MAX sdl_ohosvideo_OH_XCOMPONENT_ID_LEN_MAX +#define OH_MAX_TOUCH_POINTS_NUMBER sdl_ohosvideo_OH_MAX_TOUCH_POINTS_NUMBER +#include + +// !!! FIXME: move stuff in from Android code + +#include "../SDL_sysvideo.h" +#include "../SDL_pixels_c.h" +#include "../../events/SDL_events_c.h" +#include "../../events/SDL_windowevents_c.h" + +#include "SDL_openharmonyvideo.h" +#include "SDL_openharmonyopengl.h" +#include "SDL_openharmonyclipboard.h" +#include "SDL_openharmonyevents.h" +#include "SDL_openharmonykeyboard.h" +#include "SDL_openharmonymouse.h" +//#include "SDL_openharmonytouch.h" +#include "SDL_openharmonywindow.h" +#include "SDL_openharmonyvulkan.h" +//#include "SDL_openharmonymessagebox.h" + +#define OPENHARMONY_VID_DRIVER_NAME "openharmony" + +#include "../SDL_egl_c.h" +#define OPENHARMONY_GLES_GetProcAddress SDL_EGL_GetProcAddressInternal +#define OPENHARMONY_GLES_UnloadLibrary SDL_EGL_UnloadLibrary +#define OPENHARMONY_GLES_SetSwapInterval SDL_EGL_SetSwapInterval +#define OPENHARMONY_GLES_GetSwapInterval SDL_EGL_GetSwapInterval +#define OPENHARMONY_GLES_DestroyContext SDL_EGL_DestroyContext + + +static OH_NativeXComponent *native_xcomponent = NULL; +static void *native_window = NULL; + +void SDL_OpenHarmonyGetNativeWindowPointers(void **xcomponent, void **window) +{ + *xcomponent = (void *) native_xcomponent; + *window = (void *) native_window; +} + +void SDL_OpenHarmonyVideoSurfaceDestroyed(void *component, void *window) +{ + SDL_assert(native_xcomponent == ((OH_NativeXComponent *) component)); // right now we assume one surface, one window. + native_xcomponent = NULL; + native_window = NULL; +} + +void SDL_OpenHarmonyVideoSurfaceChanged(void *component, void *window) +{ + SDL_assert(native_xcomponent == ((OH_NativeXComponent *) component)); // right now we assume one surface, one window. + uint64_t w, h; + OH_NativeXComponent_GetXComponentSize(native_xcomponent, native_window, &w, &h); + SDL_SendWindowEvent(OPENHARMONY_Window, SDL_EVENT_WINDOW_RESIZED, (int) w, (int) h); +} + +void SDL_OpenHarmonyVideoSurfaceCreated(void *component, void *window) +{ + SDL_assert(native_xcomponent == NULL); // right now we assume one surface, one window. + native_xcomponent = (OH_NativeXComponent *) component; + native_window = window; +} + +static bool OPENHARMONY_SuspendScreenSaver(SDL_VideoDevice *_this) +{ + return SDL_OpenHarmonyChangeScreenSaver(!_this->suspend_screensaver); +} + +static bool OPENHARMONY_VideoInit(SDL_VideoDevice *_this) +{ + SDL_VideoData *videodata = _this->internal; + + videodata->isPaused = false; + videodata->isPausing = false; + + // !!! FIXME: eventually we'll want to enumerate displays, since you can probably plug in something + // !!! FIXME: through a USB-C to HDMI adapter, or maybe there will be separate "outer" screens vs + // !!! FIXME: an internal foldable one, but for now let's just get _something_ on _any_ display. + uint64_t dispid64 = 0; + if (OH_NativeDisplayManager_GetDefaultDisplayId(&dispid64) != DISPLAY_MANAGER_OK) { + return SDL_SetError("Couldn't get default display id"); + } + const uint32_t dispid = (uint32_t) dispid64; + + NativeDisplayManager_DisplayInfo *dispinfo = NULL; + if (OH_NativeDisplayManager_CreateDisplayById(dispid, &dispinfo) != DISPLAY_MANAGER_OK) { // (should really be called "CreateDisplayInfo", not "CreateDisplay") + return SDL_SetError("Couldn't get default display info"); + } + + SDL_DisplayMode mode; + SDL_zero(mode); + mode.format = SDL_PIXELFORMAT_BGRA8888; // !!! FIXME + mode.w = (int) dispinfo->physicalWidth; + mode.h = (int) dispinfo->physicalHeight; + mode.refresh_rate = (float) dispinfo->refreshRate; + mode.pixel_density = 1.0f; + + const NativeDisplayManager_Rotation rotation = dispinfo->rotation; + const NativeDisplayManager_Orientation orientation = dispinfo->orientation; + + OH_NativeDisplayManager_DestroyDisplay(dispinfo); + + const SDL_DisplayID displayID = SDL_AddBasicVideoDisplay(&mode); + if (displayID == 0) { + return false; + } + + SDL_VideoDisplay *display = SDL_GetVideoDisplay(displayID); + + switch (orientation) { + case DISPLAY_MANAGER_PORTRAIT: display->natural_orientation = SDL_ORIENTATION_PORTRAIT; break; + case DISPLAY_MANAGER_LANDSCAPE: display->natural_orientation = SDL_ORIENTATION_LANDSCAPE; break; + case DISPLAY_MANAGER_PORTRAIT_INVERTED: display->natural_orientation = SDL_ORIENTATION_PORTRAIT_FLIPPED; break; + case DISPLAY_MANAGER_LANDSCAPE_INVERTED: display->natural_orientation = SDL_ORIENTATION_LANDSCAPE_FLIPPED; break; + default: display->natural_orientation = SDL_ORIENTATION_UNKNOWN; break; + } + + // !!! FIXME: this is probably wrong, I think on OpenHarmony phones/tablets, these are setting both orientation and rotation, and this works out because most people are launching apps while holding the phone in portrait mode (0 rotation). + // !!! FIXME: if I'm right, we should decide if this is a phone/tablet screen and just set the natural orientation to portrait and then use this code to calculate current orientation. + if (rotation == DISPLAY_MANAGER_ROTATION_90) { // rotations are clockwise on OpenHarmony. + static const SDL_DisplayOrientation rotated[5] = { SDL_ORIENTATION_UNKNOWN, SDL_ORIENTATION_PORTRAIT, SDL_ORIENTATION_PORTRAIT_FLIPPED, SDL_ORIENTATION_LANDSCAPE_FLIPPED, SDL_ORIENTATION_LANDSCAPE }; + SDL_assert(((int) display->natural_orientation) < SDL_arraysize(rotated)); + display->current_orientation = rotated[(int) display->natural_orientation]; + } else if (rotation == DISPLAY_MANAGER_ROTATION_180) { // rotations are clockwise on OpenHarmony. + static const SDL_DisplayOrientation rotated[5] = { SDL_ORIENTATION_UNKNOWN, SDL_ORIENTATION_LANDSCAPE_FLIPPED, SDL_ORIENTATION_LANDSCAPE, SDL_ORIENTATION_PORTRAIT_FLIPPED, SDL_ORIENTATION_PORTRAIT }; + SDL_assert(((int) display->natural_orientation) < SDL_arraysize(rotated)); + display->current_orientation = rotated[(int) display->natural_orientation]; + } else if (rotation == DISPLAY_MANAGER_ROTATION_270) { // rotations are clockwise on OpenHarmony. + static const SDL_DisplayOrientation rotated[5] = { SDL_ORIENTATION_UNKNOWN, SDL_ORIENTATION_PORTRAIT_FLIPPED, SDL_ORIENTATION_PORTRAIT, SDL_ORIENTATION_LANDSCAPE, SDL_ORIENTATION_LANDSCAPE_FLIPPED }; + SDL_assert(((int) display->natural_orientation) < SDL_arraysize(rotated)); + display->current_orientation = rotated[(int) display->natural_orientation]; + } else { + display->current_orientation = display->natural_orientation; + } + + display->content_scale = mode.pixel_density; + + // !!! FIXME: look at SDL_OnApplicationDidChangeStatusBarOrientation() and do something similar. + + +// !!! FIXME +#if 0 + OPENHARMONY_InitTouch(); + OPENHARMONY_InitMouse(); +#endif + OPENHARMONY_InitClipboard(_this); + + // We're done! + return true; +} + +void OPENHARMONY_VideoQuit(SDL_VideoDevice *_this) +{ + OPENHARMONY_QuitClipboard(_this); + +// !!! FIXME +#if 0 + OPENHARMONY_QuitMouse(); + OPENHARMONY_QuitTouch(); +#endif +} + +static void OPENHARMONY_DeleteDevice(SDL_VideoDevice *device) +{ + SDL_free(device->internal); + SDL_free(device); +} + +static SDL_VideoDevice *OPENHARMONY_CreateDevice(void) +{ + SDL_VideoDevice *device; + SDL_VideoData *data; + + // Initialize all variables that we clean on shutdown + device = (SDL_VideoDevice *)SDL_calloc(1, sizeof(SDL_VideoDevice)); + if (!device) { + return NULL; + } + + data = (SDL_VideoData *)SDL_calloc(1, sizeof(SDL_VideoData)); + if (!data) { + SDL_free(device); + return NULL; + } + + device->internal = data; + device->system_theme = SDL_GetOpenHarmonySystemTheme(); + + // Set the function pointers + device->VideoInit = OPENHARMONY_VideoInit; + device->VideoQuit = OPENHARMONY_VideoQuit; + + device->CreateSDLWindow = OPENHARMONY_CreateWindow; + device->SetWindowTitle = OPENHARMONY_SetWindowTitle; + device->SetWindowFullscreen = OPENHARMONY_SetWindowFullscreen; + device->MinimizeWindow = OPENHARMONY_MinimizeWindow; + device->SetWindowResizable = OPENHARMONY_SetWindowResizable; + device->DestroyWindow = OPENHARMONY_DestroyWindow; + + device->free = OPENHARMONY_DeleteDevice; + + // GL pointers +#ifdef SDL_VIDEO_OPENGL_EGL + device->GL_LoadLibrary = OPENHARMONY_GLES_LoadLibrary; + device->GL_GetProcAddress = OPENHARMONY_GLES_GetProcAddress; + device->GL_UnloadLibrary = OPENHARMONY_GLES_UnloadLibrary; + device->GL_CreateContext = OPENHARMONY_GLES_CreateContext; + device->GL_MakeCurrent = OPENHARMONY_GLES_MakeCurrent; + device->GL_SetSwapInterval = OPENHARMONY_GLES_SetSwapInterval; + device->GL_GetSwapInterval = OPENHARMONY_GLES_GetSwapInterval; + device->GL_SwapWindow = OPENHARMONY_GLES_SwapWindow; + device->GL_DestroyContext = OPENHARMONY_GLES_DestroyContext; +#endif + +#ifdef SDL_VIDEO_VULKAN + device->Vulkan_LoadLibrary = OPENHARMONY_Vulkan_LoadLibrary; + device->Vulkan_UnloadLibrary = OPENHARMONY_Vulkan_UnloadLibrary; + device->Vulkan_GetInstanceExtensions = OPENHARMONY_Vulkan_GetInstanceExtensions; + device->Vulkan_CreateSurface = OPENHARMONY_Vulkan_CreateSurface; + device->Vulkan_DestroySurface = OPENHARMONY_Vulkan_DestroySurface; +#endif + + // Screensaver + device->SuspendScreenSaver = OPENHARMONY_SuspendScreenSaver; + + device->PumpEvents = OPENHARMONY_PumpEvents; + + // Screen keyboard + device->HasScreenKeyboardSupport = OPENHARMONY_HasScreenKeyboardSupport; + device->ShowScreenKeyboard = OPENHARMONY_ShowScreenKeyboard; + device->HideScreenKeyboard = OPENHARMONY_HideScreenKeyboard; + + // Clipboard + device->GetTextMimeTypes = OPENHARMONY_GetTextMimeTypes; + device->SetClipboardText = OPENHARMONY_SetClipboardText; + device->GetClipboardText = OPENHARMONY_GetClipboardText; + device->HasClipboardText = OPENHARMONY_HasClipboardText; + + device->device_caps = VIDEO_DEVICE_CAPS_SENDS_FULLSCREEN_DIMENSIONS; + + return device; +} + +VideoBootStrap OPENHARMONY_bootstrap = { + OPENHARMONY_VID_DRIVER_NAME, "SDL OpenHarmony/HarmonyOS video driver", + OPENHARMONY_CreateDevice, + /*!!! FIXME OPENHARMONY_ShowMessageBox*/ NULL, + false +}; + +#endif // SDL_VIDEO_DRIVER_OPENHARMONY diff --git a/src/video/openharmony/SDL_openharmonyvideo.h b/src/video/openharmony/SDL_openharmonyvideo.h new file mode 100644 index 0000000000..28f0d5901e --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyvideo.h @@ -0,0 +1,47 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_openharmonyvideo_h_ +#define SDL_openharmonyvideo_h_ + +#include "../SDL_sysvideo.h" + +// these are fired from XComponent callbacks registered in src/core/openharmony/SDL_openharmony.c +extern void SDL_OpenHarmonyVideoSurfaceDestroyed(void *component, void *window); +extern void SDL_OpenHarmonyVideoSurfaceChanged(void *component, void *window); +extern void SDL_OpenHarmonyVideoSurfaceCreated(void *component, void *window); + +// Get the app's current native window. +extern void SDL_OpenHarmonyGetNativeWindowPointers(void **xcomponent, void **window); + +struct SDL_VideoData +{ + void *oh_pasteboard; + void *oh_pasteboard_observer; + bool clipboard_set; + +// !!! FIXME: Android stuff. + int isPaused; + int isPausing; +}; + +#endif // SDL_openharmonyvideo_h_ diff --git a/src/video/openharmony/SDL_openharmonyvulkan.c b/src/video/openharmony/SDL_openharmonyvulkan.c new file mode 100644 index 0000000000..3cafbd73e3 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyvulkan.c @@ -0,0 +1,174 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +/* + * Copied from the Android implementation, which was done by Mark Callow, + * www.edgewise-consulting.com, and originally based on Jacob Lifshay's SDL_x11vulkan.h. + */ + +#include "SDL_internal.h" + +#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_OPENHARMONY) + +#include "../SDL_vulkan_internal.h" + +#include "SDL_openharmonyvideo.h" +#include "SDL_openharmonywindow.h" + +#include "SDL_openharmonyvulkan.h" + + +bool OPENHARMONY_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path) +{ + VkExtensionProperties *extensions = NULL; + Uint32 extensionCount = 0; + bool hasSurfaceExtension = false; + bool hasOHOSSurfaceExtension = false; + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = NULL; + + if (_this->vulkan_config.loader_handle) { + return SDL_SetError("Vulkan already loaded"); + } + + // Load the Vulkan loader library + if (!path) { + path = SDL_GetHint(SDL_HINT_VULKAN_LIBRARY); + if (!path) { + path = "libvulkan.so"; + } + } + + SDL_SharedObject *lib = SDL_LoadObject(path); + _this->vulkan_config.loader_handle = lib; + if (!lib) { + return false; + } + + SDL_strlcpy(_this->vulkan_config.loader_path, path, sizeof (_this->vulkan_config.loader_path)); + vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr) SDL_LoadFunction(lib, "vkGetInstanceProcAddr"); + if (!vkGetInstanceProcAddr) { + goto fail; + } + + _this->vulkan_config.vkGetInstanceProcAddr = (void *)vkGetInstanceProcAddr; + _this->vulkan_config.vkEnumerateInstanceExtensionProperties = (void *)((PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr)(VK_NULL_HANDLE, "vkEnumerateInstanceExtensionProperties"); + if (!_this->vulkan_config.vkEnumerateInstanceExtensionProperties) { + goto fail; + } + + extensions = SDL_Vulkan_CreateInstanceExtensionsList((PFN_vkEnumerateInstanceExtensionProperties) _this->vulkan_config.vkEnumerateInstanceExtensionProperties, &extensionCount); + if (!extensions) { + goto fail; + } + + for (Uint32 i = 0; i < extensionCount; i++) { + if (SDL_strcmp(VK_KHR_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { + hasSurfaceExtension = true; + } else if (SDL_strcmp(VK_OHOS_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { + hasOHOSSurfaceExtension = true; + } + } + + SDL_free(extensions); + + if (!hasSurfaceExtension) { + SDL_SetError("Installed Vulkan doesn't implement the " VK_KHR_SURFACE_EXTENSION_NAME " extension"); + goto fail; + } else if (!hasOHOSSurfaceExtension) { + SDL_SetError("Installed Vulkan doesn't implement the " VK_OHOS_SURFACE_EXTENSION_NAME " extension"); + goto fail; + } + + return true; + +fail: + SDL_UnloadObject(_this->vulkan_config.loader_handle); + _this->vulkan_config.loader_handle = NULL; + return false; +} + +void OPENHARMONY_Vulkan_UnloadLibrary(SDL_VideoDevice *_this) +{ + if (_this->vulkan_config.loader_handle) { + SDL_UnloadObject(_this->vulkan_config.loader_handle); + _this->vulkan_config.loader_handle = NULL; + } +} + +char const * const *OPENHARMONY_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this, Uint32 *count) +{ + static const char *const extensionsForOpenHarmony[] = { + VK_KHR_SURFACE_EXTENSION_NAME, VK_OHOS_SURFACE_EXTENSION_NAME + }; + if (count) { + *count = SDL_arraysize(extensionsForOpenHarmony); + } + return extensionsForOpenHarmony; +} + +bool OPENHARMONY_Vulkan_CreateSurface(SDL_VideoDevice *_this, + SDL_Window *window, + VkInstance instance, + const struct VkAllocationCallbacks *allocator, + VkSurfaceKHR *surface) +{ + SDL_WindowData *windowData = window->internal; + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr; + PFN_vkCreateSurfaceOHOS vkCreateSurfaceOHOS = (PFN_vkCreateSurfaceOHOS ) vkGetInstanceProcAddr(instance, "vkCreateSurfaceOHOS"); + VkSurfaceCreateInfoOHOS createInfo; + VkResult result; + + if (!_this->vulkan_config.loader_handle) { + return SDL_SetError("Vulkan is not loaded"); + } + + if (!vkCreateSurfaceOHOS) { + return SDL_SetError(VK_OHOS_SURFACE_EXTENSION_NAME " extension is not enabled in the Vulkan instance."); + } + + SDL_zero(createInfo); + createInfo.sType = VK_STRUCTURE_TYPE_SURFACE_CREATE_INFO_OHOS; + createInfo.pNext = NULL; + createInfo.flags = 0; + createInfo.window = (OHNativeWindow *) windowData->native_window; + result = vkCreateSurfaceOHOS(instance, &createInfo, allocator, surface); + if (result != VK_SUCCESS) { + if (result == VK_ERROR_NATIVE_WINDOW_IN_USE_KHR) { + return SDL_SetError("vkCreateSurfaceOHOS failed: %s, was the window created with SDL_WINDOW_VULKAN?", SDL_Vulkan_GetResultString(result)); + } else { + return SDL_SetError("vkCreateSurfaceOHOS failed: %s", SDL_Vulkan_GetResultString(result)); + } + } + return true; +} + +void OPENHARMONY_Vulkan_DestroySurface(SDL_VideoDevice *_this, + VkInstance instance, + VkSurfaceKHR surface, + const struct VkAllocationCallbacks *allocator) +{ + if (_this->vulkan_config.loader_handle) { + SDL_Vulkan_DestroySurface_Internal(_this->vulkan_config.vkGetInstanceProcAddr, instance, surface, allocator); + } +} + +#endif + diff --git a/src/video/openharmony/SDL_openharmonyvulkan.h b/src/video/openharmony/SDL_openharmonyvulkan.h new file mode 100644 index 0000000000..1643fd76b6 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyvulkan.h @@ -0,0 +1,52 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +/* + * Copied from the Android implementation, which was done by Mark Callow, + * www.edgewise-consulting.com, and originally based on Jacob Lifshay's SDL_x11vulkan.h. + */ + +#include "SDL_internal.h" + +#ifndef SDL_openharmonyvulkan_h_ +#define SDL_openharmonyvulkan_h_ + +#include "../SDL_vulkan_internal.h" +#include "../SDL_sysvideo.h" + +#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_OPENHARMONY) + +extern bool OPENHARMONY_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path); +extern void OPENHARMONY_Vulkan_UnloadLibrary(SDL_VideoDevice *_this); +extern char const * const *OPENHARMONY_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this, Uint32 *count); +extern bool OPENHARMONY_Vulkan_CreateSurface(SDL_VideoDevice *_this, + SDL_Window *window, + VkInstance instance, + const struct VkAllocationCallbacks *allocator, + VkSurfaceKHR *surface); +extern void OPENHARMONY_Vulkan_DestroySurface(SDL_VideoDevice *_this, + VkInstance instance, + VkSurfaceKHR surface, + const struct VkAllocationCallbacks *allocator); + +#endif + +#endif // SDL_openharmonyvulkan_h_ diff --git a/src/video/openharmony/SDL_openharmonywindow.c b/src/video/openharmony/SDL_openharmonywindow.c new file mode 100644 index 0000000000..f462d9d74b --- /dev/null +++ b/src/video/openharmony/SDL_openharmonywindow.c @@ -0,0 +1,187 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_OPENHARMONY + +// !!! FIXME: these are defined as "const uint32_t VARNAME = VALUE;" in native_interface_xcomponent.h, which becomes a global variable in _our_ C code! Maybe C++ handles this differently...? +#define OH_XCOMPONENT_ID_LEN_MAX sdl_ohoswindow_OH_XCOMPONENT_ID_LEN_MAX +#define OH_MAX_TOUCH_POINTS_NUMBER sdl_ohoswindow_OH_MAX_TOUCH_POINTS_NUMBER +#include + +#include "../SDL_sysvideo.h" +#include "../../events/SDL_keyboard_c.h" +#include "../../events/SDL_mouse_c.h" +#include "../../events/SDL_windowevents_c.h" +#include "../../core/openharmony/SDL_openharmony.h" + +#include "SDL_openharmonyvideo.h" +#include "SDL_openharmonyevents.h" +#include "SDL_openharmonywindow.h" + + +// Currently only one window +SDL_Window *OPENHARMONY_Window = NULL; + +bool OPENHARMONY_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props) +{ + SDL_WindowData *data; + bool result = true; + + // !!! FIXME: Android has locking here; maybe it isn't necessary for OpenHarmony? What happens if we're in a background thread? + //if (!OPENHARMONY_WaitActiveAndLockActivity()) { + // return false; + //} + + if (OPENHARMONY_Window) { + result = SDL_SetError("SDL on OpenHarmony (currently) only supports one window"); + goto endfunction; + } + + void *voidxcomp, *native_window; + SDL_OpenHarmonyGetNativeWindowPointers(&voidxcomp, &native_window); + OH_NativeXComponent *native_xcomponent = (OH_NativeXComponent *) voidxcomp; + if (!native_xcomponent || !native_window) { + result = SDL_SetError("Don't have Native XComponent or Native Window"); + goto endfunction; + } + +#if 0 // !!! FIXME + // Set orientation + Android_JNI_SetOrientation(window->w, window->h, window->flags & SDL_WINDOW_RESIZABLE, SDL_GetHint(SDL_HINT_ORIENTATIONS)); +#endif + + uint64_t w, h; + OH_NativeXComponent_GetXComponentSize(native_xcomponent, native_window, &w, &h); + + // Adjust the window data to match the screen + window->x = 0; + window->y = 0; + window->w = (int) w; + window->h = (int) h; + + // One window, it always has focus + SDL_SetMouseFocus(window); + SDL_SetKeyboardFocus(window); + + data = (SDL_WindowData *)SDL_calloc(1, sizeof(*data)); + if (!data) { + result = false; + goto endfunction; + } + + data->native_xcomponent = native_xcomponent; + data->native_window = native_window; + SDL_SetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_OPENHARMONY_XCOMPONENT_POINTER, data->native_xcomponent); + SDL_SetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_OPENHARMONY_WINDOW_POINTER, data->native_window); + + /* Do not create EGLSurface for Vulkan window since it will then make the window + incompatible with vkCreateAndroidSurfaceKHR */ +#ifdef SDL_VIDEO_OPENGL_EGL + if (window->flags & SDL_WINDOW_OPENGL) { + data->egl_surface = SDL_EGL_CreateSurface(_this, window, (NativeWindowType)data->native_window); + if (data->egl_surface == EGL_NO_SURFACE) { + SDL_free(data); + result = false; + goto endfunction; + } + } + SDL_SetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_OPENHARMONY_SURFACE_POINTER, data->egl_surface); +#endif + +#if 0 // !!! FIXME + SDL_SetWindowSafeAreaInsets(window, Android_SafeInsetLeft, Android_SafeInsetRight, Android_SafeInsetTop, Android_SafeInsetBottom); +#endif + + window->internal = data; + OPENHARMONY_Window = window; + +endfunction: + + // !!! FIXME: Android has locking here; maybe it isn't necessary for OpenHarmony? What happens if we're in a background thread? + //Android_UnlockActivityMutex(); + + return result; +} + +void OPENHARMONY_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window) +{ +#if 0 // !!! FIXME + Android_JNI_SetActivityTitle(window->title); +#endif +} + +SDL_FullscreenResult OPENHARMONY_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen) +{ + if (window == OPENHARMONY_Window) { + const bool show_bars = (fullscreen == SDL_FULLSCREEN_OP_LEAVE); + if (!SDL_OpenHarmonyToggleSystemBars(show_bars, show_bars)) { + return SDL_FULLSCREEN_FAILED; + } + // !!! FIXME: make sure this causes an XComponent resize event, that we can pick up later. + return SDL_FULLSCREEN_PENDING; + } + return SDL_FULLSCREEN_FAILED; +} + +void OPENHARMONY_MinimizeWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ +#if 0 // !!! FIXME + Android_JNI_MinimizeWindow(); +#endif +} + +void OPENHARMONY_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable) +{ +#if 0 // !!! FIXME + // Set orientation + Android_JNI_SetOrientation(window->w, window->h, window->flags & SDL_WINDOW_RESIZABLE, SDL_GetHint(SDL_HINT_ORIENTATIONS)); +#endif +} + +void OPENHARMONY_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + // !!! FIXME: Android has locking here; maybe it isn't necessary for OpenHarmony? What happens if we're in a background thread? + //Android_LockActivityMutex(); + + if (window == OPENHARMONY_Window) { + OPENHARMONY_Window = NULL; + + if (window->internal) { + SDL_WindowData *data = window->internal; + +#ifdef SDL_VIDEO_OPENGL_EGL + if (data->egl_surface != EGL_NO_SURFACE) { + SDL_EGL_DestroySurface(_this, data->egl_surface); + } +#endif + + // the XComponent and native window live past the SDL_Window; it can be reused for future windows. + + SDL_free(window->internal); + window->internal = NULL; + } + } + + //Android_UnlockActivityMutex(); +} + +#endif // SDL_VIDEO_DRIVER_OPENHARMONY diff --git a/src/video/openharmony/SDL_openharmonywindow.h b/src/video/openharmony/SDL_openharmonywindow.h new file mode 100644 index 0000000000..07515eb428 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonywindow.h @@ -0,0 +1,51 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_openharmonywindow_h_ +#define SDL_openharmonywindow_h_ + +#include "../../core/openharmony/SDL_openharmony.h" +#include "../SDL_egl_c.h" + +extern bool OPENHARMONY_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props); +extern void OPENHARMONY_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window); +extern SDL_FullscreenResult OPENHARMONY_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen); +extern void OPENHARMONY_MinimizeWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void OPENHARMONY_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable); + +extern void OPENHARMONY_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern SDL_Window *OPENHARMONY_Window; + +struct SDL_WindowData +{ +#ifdef SDL_VIDEO_OPENGL_EGL + EGLSurface egl_surface; + EGLContext egl_context; // We use this to preserve the context when losing focus + int has_swap_interval; // Save/Restore the swap interval / vsync + int swap_interval; +#endif + bool backup_done; + void *native_xcomponent; + void *native_window; +}; + +#endif // SDL_openharmonywindow_h_ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9e79d58d52..8400152dc2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -167,7 +167,7 @@ function(add_sdl_test_executable TARGET) if(AST_NEEDS_RESOURCES) list(APPEND EXTRA_SOURCES ${RESOURCE_FILES}) endif() - if(ANDROID) + if(ANDROID OR OHOS) add_library(${TARGET} SHARED ${AST_SOURCES} ${EXTRA_SOURCES}) else() add_executable(${TARGET} ${AST_SOURCES} ${EXTRA_SOURCES}) @@ -489,7 +489,7 @@ if(EMSCRIPTEN) endif() add_sdl_test_executable(testbounds NONINTERACTIVE SOURCES testbounds.c NAME83 bounds) add_sdl_test_executable(testcustomcursor SOURCES testcustomcursor.c NAME83 custcurs) -add_sdl_test_executable(testvulkan SOURCES testvulkan.c NAME83 vulkan) +add_sdl_test_executable(testvulkan MAIN_CALLBACKS SOURCES testvulkan.c NAME83 vulkan) add_sdl_test_executable(testoffscreen SOURCES testoffscreen.c NAME83 offscrn) add_sdl_test_executable(testpopup SOURCES testpopup.c NAME83 popup) add_sdl_test_executable(testdialog SOURCES testdialog.c NAME83 dialog) diff --git a/test/testclipboard.c b/test/testclipboard.c index 8fa8756c9a..dc8e71135e 100644 --- a/test/testclipboard.c +++ b/test/testclipboard.c @@ -62,6 +62,10 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { switch (event->type) { + case SDL_EVENT_MOUSE_BUTTON_DOWN: + SDL_SetClipboardData(ClipboardDataCallback, NULL, NULL, mime_types, SDL_arraysize(mime_types)); + break; + case SDL_EVENT_KEY_DOWN: if (event->key.key == SDLK_ESCAPE) { return SDL_APP_SUCCESS; @@ -223,7 +227,7 @@ SDL_AppResult SDL_AppIterate(void *appstate) SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); float x = 4.0f; float y = 4.0f; - SDL_RenderDebugText(renderer, x, y, "Press Ctrl+C to copy content to the clipboard (Alt+C to clear)"); + SDL_RenderDebugText(renderer, x, y, "Press Ctrl+C (or tap/click) to copy content to the clipboard (Alt+C to clear)"); y += SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE * 2; SDL_RenderDebugText(renderer, x, y, "Press Ctrl+P to set the primary selection text (Alt+P to clear)"); y += SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE * 2; diff --git a/test/testsymbols.c b/test/testsymbols.c index 3b5591e6ac..165c4facea 100644 --- a/test/testsymbols.c +++ b/test/testsymbols.c @@ -34,6 +34,12 @@ extern SDL_DECLSPEC void SDLCALL SDL_SendAndroidMessage(void); extern SDL_DECLSPEC void SDLCALL SDL_ShowAndroidToast(void); #endif +#if !defined(SDL_PLATFORM_OPENHARMONY) +extern SDL_DECLSPEC int SDLCALL SDL_GetOpenHarmonySDKVersion(void); +extern SDL_DECLSPEC void SDLCALL SDL_GetOpenHarmonyInternalStoragePath(void); +extern SDL_DECLSPEC bool SDLCALL SDL_RequestOpenHarmonyPermission(void); +#endif + #if !defined(SDL_PLATFORM_GDK) extern SDL_DECLSPEC void SDLCALL SDL_GDKResumeGPU(void); extern SDL_DECLSPEC void SDLCALL SDL_GDKSuspendGPU(void); diff --git a/test/testurl.c b/test/testurl.c index a6fd9c5ec4..ef61eb2f62 100644 --- a/test/testurl.c +++ b/test/testurl.c @@ -68,7 +68,9 @@ int main(int argc, char **argv) while (!quit) { while (SDL_PollEvent(&event)) { - if (event.type == SDL_EVENT_KEY_DOWN) { + if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + tryOpenURL(url); + } else if (event.type == SDL_EVENT_KEY_DOWN) { if (event.key.key == SDLK_SPACE) { tryOpenURL(url); } else if (event.key.key == SDLK_ESCAPE) { diff --git a/test/testvulkan.c b/test/testvulkan.c index f924ef55d6..f57f194437 100644 --- a/test/testvulkan.c +++ b/test/testvulkan.c @@ -9,19 +9,26 @@ including commercial applications, and to alter it and redistribute it freely. */ -#include -#include +#define SDL_MAIN_USE_CALLBACKS 1 +#include #include +#include + +#include #if defined(SDL_PLATFORM_ANDROID) && defined(__ARM_EABI__) && !defined(__ARM_ARCH_7A__) -int main(int argc, char *argv[]) +SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "No Vulkan support on this system"); - return 1; + return SDL_APP_FAILURE; } +SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { return SDL_APP_FAILURE; } +SDL_AppResult SDL_AppIterate(void *appstate) { return SDL_APP_FAILURE; } +void SDL_AppQuit(void *appstate, SDL_AppResult result) {} + #else #define VK_NO_PROTOTYPES @@ -1144,19 +1151,18 @@ static bool render(void) return true; } -int main(int argc, char **argv) +static Uint32 frames; +static Uint64 then; + +SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) { - int done; const SDL_DisplayMode *mode; - SDL_Event event; - Uint64 then, now; - Uint32 frames; int dw, dh; /* Initialize test framework */ state = SDLTest_CommonCreateState(argv, SDL_INIT_VIDEO); if (!state) { - return 1; + return SDL_APP_FAILURE; } /* Set Vulkan parameters */ @@ -1165,7 +1171,7 @@ int main(int argc, char **argv) if (!SDLTest_CommonDefaultArgs(state, argc, argv) || !SDLTest_CommonInit(state)) { SDLTest_CommonQuit(state); - return 1; + return SDL_APP_FAILURE; } mode = SDL_GetCurrentDisplayMode(SDL_GetPrimaryDisplay()); @@ -1183,40 +1189,52 @@ int main(int argc, char **argv) /* Main render loop */ frames = 0; then = SDL_GetTicks(); - done = 0; - while (!done) { - /* Check for events */ - frames++; - while (SDL_PollEvent(&event)) { - /* Need to destroy the swapchain before the window created - * by SDL. - */ - if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { - destroySwapchainAndSwapchainSpecificStuff(true); - } - SDLTest_CommonEvent(state, &event, &done); - } - if (!done) { - int i; - for (i = 0; i < state->num_windows; ++i) { - if (state->windows[i]) { - vulkanContext = &vulkanContexts[i]; - render(); - } - } + return SDL_APP_CONTINUE; +} + +SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) +{ + int done = 0; + + /* Need to destroy the swapchain before the window created + * by SDL. + */ + if (event->type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { + destroySwapchainAndSwapchainSpecificStuff(true); + } + + SDLTest_CommonEvent(state, event, &done); + return done ? SDL_APP_SUCCESS : SDL_APP_CONTINUE; +} + +SDL_AppResult SDL_AppIterate(void *appstate) +{ + int i; + + frames++; + for (i = 0; i < state->num_windows; ++i) { + if (state->windows[i]) { + vulkanContext = &vulkanContexts[i]; + render(); } } + return SDL_APP_CONTINUE; +} + +void SDL_AppQuit(void *appstate, SDL_AppResult result) +{ /* Print out some timing information */ - now = SDL_GetTicks(); - if (now > then) { - SDL_Log("%2.2f frames per second", ((double)frames * 1000) / (now - then)); + if (result != SDL_APP_FAILURE) { + const Uint64 now = SDL_GetTicks(); + if (now > then) { + SDL_Log("%2.2f frames per second", ((double)frames * 1000) / (now - then)); + } } shutdownVulkan(true); SDLTest_CommonQuit(state); - return 0; } #endif