From efb87e4b0c391623b2d8bf9fd2b93a9ffe2db89a Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 8 May 2026 17:15:32 +0200 Subject: [PATCH 01/17] CMake: SDL3 is the new default for Desktop/Mobile/Web Bump required version to 3.4.0. That version added some features which will be required for the GPU renderer. --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 03a6d5b39d..0aec491985 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -552,7 +552,7 @@ elseif(AMIGA) elseif(PS4) set(PLAYER_TARGET_PLATFORM "SDL2" CACHE STRING "Platform to compile for.") else() - set(PLAYER_TARGET_PLATFORM "SDL2" CACHE STRING "Platform to compile for. Options: SDL3 SDL2 SDL1 libretro") + set(PLAYER_TARGET_PLATFORM "SDL3" CACHE STRING "Platform to compile for. Options: SDL3 SDL2 SDL1 libretro") set_property(CACHE PLAYER_TARGET_PLATFORM PROPERTY STRINGS SDL3 SDL2 SDL1 libretro) endif() set(PLAYER_BUILD_EXECUTABLE ON) @@ -578,6 +578,7 @@ if(PLAYER_TARGET_PLATFORM STREQUAL "SDL3") target_compile_definitions(${PROJECT_NAME} PUBLIC USE_SDL=3) player_find_package(NAME SDL3 + VERSION 3.4.0 TARGET SDL3::SDL3 REQUIRED) From 5450ca980dfeba7ef90f2f3cbd239dc59cd25a8e Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 8 May 2026 17:22:20 +0200 Subject: [PATCH 02/17] SDL3 UI: Call the renderer SDL3 :) --- src/platform/sdl/sdl3_ui.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/platform/sdl/sdl3_ui.cpp b/src/platform/sdl/sdl3_ui.cpp index 1c5a28806c..f373a0a8b1 100644 --- a/src/platform/sdl/sdl3_ui.cpp +++ b/src/platform/sdl/sdl3_ui.cpp @@ -1174,13 +1174,13 @@ int FilterUntilFocus(const SDL_Event* evnt) { void Sdl3Ui::vGetConfig(Game_ConfigVideo& cfg) const { #ifdef __EMSCRIPTEN__ - cfg.renderer.Lock("SDL2 (Software, Emscripten)"); + cfg.renderer.Lock("SDL3 (Software, Emscripten)"); #elif defined(__wii__) - cfg.renderer.Lock("SDL2 (Software, Wii)"); + cfg.renderer.Lock("SDL3 (Software, Wii)"); #elif defined(__WIIU__) - cfg.renderer.Lock("SDL2 (Software, Wii U)"); + cfg.renderer.Lock("SDL3 (Software, Wii U)"); #else - cfg.renderer.Lock("SDL2 (Software)"); + cfg.renderer.Lock("SDL3 (Software)"); #endif cfg.vsync.SetOptionVisible(true); From 7851f957ce6bd561137ce2a6a9064454276b1a1e Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 8 May 2026 17:23:19 +0200 Subject: [PATCH 03/17] SDL3 UI: Remove old workarounds Guess they are not required anymore? --- src/platform/sdl/sdl3_ui.cpp | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/platform/sdl/sdl3_ui.cpp b/src/platform/sdl/sdl3_ui.cpp index f373a0a8b1..2e7e74c562 100644 --- a/src/platform/sdl/sdl3_ui.cpp +++ b/src/platform/sdl/sdl3_ui.cpp @@ -283,26 +283,6 @@ bool Sdl3Ui::RefreshDisplayMode() { #endif if (!sdl_window) { - #ifdef __ANDROID__ - // Workaround SDL bug: https://bugzilla.libsdl.org/show_bug.cgi?id=2291 - // Set back buffer format to 565 - SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5); - SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6); - SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5); - #endif - - #if defined(__APPLE__) && TARGET_OS_OSX - // Use OpenGL on Mac only -- to work around an SDL Metal deficiency - // where it will always use discrete GPU. - // See SDL source code: - // http://hg.libsdl.org/SDL/file/aa9d7c43a982/src/render/metal/SDL_render_metal.m#l1613 - SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl"); - #endif - - #if defined(__EMSCRIPTEN__) || defined(_WIN32) - flags |= SDL_WINDOW_HIGH_PIXEL_DENSITY; - #endif - // Create our window SDL_PropertiesID wprops = SDL_CreateProperties(); SDL_SetStringProperty(wprops, SDL_PROP_WINDOW_CREATE_TITLE_STRING, GAME_TITLE); From bed3130a12394ebf48acb4aed9ac519c8ffd4479 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 8 May 2026 18:19:25 +0200 Subject: [PATCH 04/17] SDL3: Readd format detection (ported over from SDL2) --- src/platform/sdl/sdl3_ui.cpp | 91 +++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/src/platform/sdl/sdl3_ui.cpp b/src/platform/sdl/sdl3_ui.cpp index 2e7e74c562..2a4d81caa1 100644 --- a/src/platform/sdl/sdl3_ui.cpp +++ b/src/platform/sdl/sdl3_ui.cpp @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with EasyRPG Player. If not, see . */ + #include #include #include @@ -68,6 +69,38 @@ static SDL_PixelFormat GetDefaultFormat() { #endif } +/** + * Return preference for the given sdl format. + * Higher numbers are better, -1 means unsupported. + * We prefer formats which have fast paths in pixman. + */ +static int GetFormatRank(uint32_t fmt) { + + switch (fmt) { +#ifdef WORDS_BIGENDIAN + case SDL_PIXELFORMAT_RGBA32: + return 0; + case SDL_PIXELFORMAT_BGRA32: + return 0; + case SDL_PIXELFORMAT_ARGB32: + return 1; + case SDL_PIXELFORMAT_ABGR32: + return 2; +#else + case SDL_PIXELFORMAT_RGBA32: + return 2; + case SDL_PIXELFORMAT_BGRA32: + return 2; + case SDL_PIXELFORMAT_ARGB32: + return 1; + case SDL_PIXELFORMAT_ABGR32: + return 0; +#endif + default: + return -1; + } +} + static DynamicFormat GetDynamicFormat(uint32_t fmt) { switch (fmt) { case SDL_PIXELFORMAT_RGBA32: @@ -83,6 +116,34 @@ static DynamicFormat GetDynamicFormat(uint32_t fmt) { } } +static SDL_PixelFormat SelectFormat(const SDL_PixelFormat* formats, bool print_all) { + SDL_PixelFormat current_fmt = SDL_PIXELFORMAT_UNKNOWN; + int current_rank = -1; + + if (!formats) { + return current_fmt; + } + + for (int i = 0; formats[i] != SDL_PIXELFORMAT_UNKNOWN; ++i) { + const auto fmt = formats[i]; + int rank = GetFormatRank(fmt); + if (rank >= 0) { + if (rank > current_rank) { + current_fmt = fmt; + current_rank = rank; + } + Output::Debug("SDL3: Detected format ({}) {} : rank=({})", + i, SDL_GetPixelFormatName(fmt), rank); + } else { + if (print_all) { + Output::Debug("SDL3: Detected format ({}) {} : Not Supported", + i, SDL_GetPixelFormatName(fmt)); + } + } + } + return current_fmt; +} + #ifdef _WIN32 HWND GetWindowHandle(SDL_Window* window) { return (HWND)SDL_GetPointerProperty( @@ -334,7 +395,27 @@ bool Sdl3Ui::RefreshDisplayMode() { sdl_renderer = nullptr; }); - texture_format = GetDefaultFormat(); + SDL_PropertiesID render_props = SDL_GetRendererProperties(sdl_renderer); + const SDL_PixelFormat* texture_formats = nullptr; + if (render_props != 0) { + Output::Debug("SDL3: RendererInfo name={} vsync={}", + SDL_GetStringProperty(render_props, SDL_PROP_RENDERER_NAME_STRING, "Unknown"), + SDL_GetNumberProperty(render_props, SDL_PROP_RENDERER_VSYNC_NUMBER, -1) + ); + texture_formats = reinterpret_cast( + SDL_GetPointerProperty(render_props, SDL_PROP_RENDERER_TEXTURE_FORMATS_POINTER, nullptr)); + texture_format = SelectFormat(texture_formats, false); + } else { + Output::Debug("SDL_GetRendererProperties failed : {}", SDL_GetError()); + } + + if (texture_format == SDL_PIXELFORMAT_UNKNOWN) { + texture_format = GetDefaultFormat(); + Output::Debug("SDL3: None of the detected formats were supported! Falling back to {}. This will likely cause performance degredation.", + SDL_GetPixelFormatName(texture_format)); + // Run again to print all the formats on this system. + SelectFormat(texture_formats, true); + } Output::Debug("SDL3: Selected Pixel Format {}", SDL_GetPixelFormatName(texture_format)); @@ -385,7 +466,13 @@ bool Sdl3Ui::RefreshDisplayMode() { DwmSetWindowAttribute(window, 33 /* DWMWA_WINDOW_CORNER_PREFERENCE */, &window_rounding, sizeof(window_rounding)); #endif - uint32_t sdl_pixel_fmt = GetDefaultFormat(); + uint32_t sdl_pixel_fmt; + if (auto tex_props = SDL_GetTextureProperties(sdl_texture_game); tex_props != 0) { + sdl_pixel_fmt = SDL_GetNumberProperty(tex_props, SDL_PROP_TEXTURE_FORMAT_NUMBER, GetDefaultFormat()); + } else { + Output::Debug("SDL_GetTextureProperties failed : {}", SDL_GetError()); + return false; + } auto format = GetDynamicFormat(sdl_pixel_fmt); Bitmap::SetFormat(Bitmap::ChooseFormat(format)); From 5c0b4b5178c082d9a4d41d31d9e7db5083503b87 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 8 May 2026 18:20:48 +0200 Subject: [PATCH 05/17] Emscripten: Force canvas width and height to prevent SDL3 from overwriting it This appears to be a regression (reported as SDL#15502) --- resources/emscripten/emscripten-shell.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/emscripten/emscripten-shell.html b/resources/emscripten/emscripten-shell.html index c10399edc9..ebe713eb99 100644 --- a/resources/emscripten/emscripten-shell.html +++ b/resources/emscripten/emscripten-shell.html @@ -47,6 +47,7 @@ #controls button { -webkit-appearance: button; + appearance: button; display: inline-flex; background: transparent; border: 0; @@ -66,8 +67,8 @@ position: absolute; top: 50%; left: 50%; - width: 100%; - height: 100%; + width: 100vw !important; + height: 100vh !important; border: 0; image-rendering: pixelated; image-rendering: crisp-edges; From 5f4810fdbf363a4682cae090a489e6321af4fa26 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 4 Apr 2026 14:13:40 +0200 Subject: [PATCH 06/17] SDL3 added SDL_SCALEMODE_PIXELART which removes the need for the scaled render target --- src/platform/sdl/sdl3_ui.cpp | 56 +++++++----------------------------- src/platform/sdl/sdl3_ui.h | 1 - 2 files changed, 11 insertions(+), 46 deletions(-) diff --git a/src/platform/sdl/sdl3_ui.cpp b/src/platform/sdl/sdl3_ui.cpp index 2a4d81caa1..3a284bd911 100644 --- a/src/platform/sdl/sdl3_ui.cpp +++ b/src/platform/sdl/sdl3_ui.cpp @@ -220,9 +220,6 @@ Sdl3Ui::~Sdl3Ui() { if (sdl_texture_game) { SDL_DestroyTexture(sdl_texture_game); } - if (sdl_texture_scaled) { - SDL_DestroyTexture(sdl_texture_scaled); - } if (sdl_renderer) { SDL_DestroyRenderer(sdl_renderer); } @@ -253,7 +250,9 @@ bool Sdl3Ui::vChangeDisplaySurfaceResolution(int new_width, int new_height) { } sdl_texture_game = new_sdl_texture_game; - SDL_SetTextureScaleMode(sdl_texture_game, SDL_SCALEMODE_NEAREST); + auto scaling_mode = (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear) ? + SDL_SCALEMODE_PIXELART : SDL_SCALEMODE_NEAREST; + SDL_SetTextureScaleMode(sdl_texture_game, scaling_mode); BitmapRef new_main_surface = Bitmap::Create(new_width, new_height, Color(0, 0, 0, 255)); @@ -433,7 +432,9 @@ bool Sdl3Ui::RefreshDisplayMode() { return false; } - SDL_SetTextureScaleMode(sdl_texture_game, SDL_SCALEMODE_NEAREST); + auto scaling_mode = (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear) ? + SDL_SCALEMODE_PIXELART : SDL_SCALEMODE_NEAREST; + SDL_SetTextureScaleMode(sdl_texture_game, scaling_mode); renderer_sg.Dismiss(); window_sg.Dismiss(); @@ -558,6 +559,9 @@ bool Sdl3Ui::ProcessEvents() { void Sdl3Ui::SetScalingMode(ConfigEnum::ScalingMode mode) { window.size_changed = true; + auto scaling_mode = (mode == ConfigEnum::ScalingMode::Bilinear) ? + SDL_SCALEMODE_PIXELART : SDL_SCALEMODE_NEAREST; + SDL_SetTextureScaleMode(sdl_texture_game, scaling_mode); vcfg.scaling_mode.Set(mode); } @@ -584,24 +588,6 @@ void Sdl3Ui::SetScreenScale(int scale) { } void Sdl3Ui::UpdateDisplay() { -#ifdef __WIIU__ - if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && window.scale > 0.f) { - // Workaround WiiU bug: Bilinear uses a render target and for these the format is not converted - void* target_pixels; - int target_pitch; - - SDL_LockTexture(sdl_texture_game, nullptr, &target_pixels, &target_pitch); - SDL_ConvertPixels(main_surface->width(), main_surface->height(), GetDefaultFormat(), main_surface->pixels(), - main_surface->pitch(), SDL_PIXELFORMAT_RGBA8888, target_pixels, target_pitch); - SDL_UnlockTexture(sdl_texture_game); - } else { - SDL_UpdateTexture(sdl_texture_game, nullptr, main_surface->pixels(), main_surface->pitch()); - } -#else - // SDL_UpdateTexture was found to be faster than SDL_LockTexture / SDL_UnlockTexture. - SDL_UpdateTexture(sdl_texture_game, nullptr, main_surface->pixels(), main_surface->pitch()); -#endif - if (window.size_changed && window.width > 0 && window.height > 0) { // Based on SDL2 function UpdateLogicalSize window.size_changed = false; @@ -659,31 +645,11 @@ void Sdl3Ui::UpdateDisplay() { do_stretch(); SDL_SetRenderViewport(sdl_renderer, &viewport); } - - if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && window.scale > 0.f) { - if (sdl_texture_scaled) { - SDL_DestroyTexture(sdl_texture_scaled); - } - sdl_texture_scaled = SDL_CreateTexture(sdl_renderer, texture_format, SDL_TEXTUREACCESS_TARGET, - static_cast(ceilf(window.scale)) * main_surface->width(), static_cast(ceilf(window.scale)) * main_surface->height()); - if (!sdl_texture_scaled) { - Output::Debug("SDL_CreateTexture failed : {}", SDL_GetError()); - } - } } + SDL_UpdateTexture(sdl_texture_game, nullptr, main_surface->pixels(), main_surface->pitch()); SDL_RenderClear(sdl_renderer); - if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && window.scale > 0.f) { - // Render game texture on the scaled texture - SDL_SetRenderTarget(sdl_renderer, sdl_texture_scaled); - SDL_RenderClear(sdl_renderer); - SDL_RenderTexture(sdl_renderer, sdl_texture_game, nullptr, nullptr); - - SDL_SetRenderTarget(sdl_renderer, nullptr); - SDL_RenderTexture(sdl_renderer, sdl_texture_scaled, nullptr, nullptr); - } else { - SDL_RenderTexture(sdl_renderer, sdl_texture_game, nullptr, nullptr); - } + SDL_RenderTexture(sdl_renderer, sdl_texture_game, nullptr, nullptr); SDL_RenderPresent(sdl_renderer); } diff --git a/src/platform/sdl/sdl3_ui.h b/src/platform/sdl/sdl3_ui.h index 482887ebde..8f387c1c22 100644 --- a/src/platform/sdl/sdl3_ui.h +++ b/src/platform/sdl/sdl3_ui.h @@ -130,7 +130,6 @@ class Sdl3Ui final : public BaseUi { /** Main SDL window. */ SDL_Texture* sdl_texture_game = nullptr; - SDL_Texture* sdl_texture_scaled = nullptr; SDL_Window* sdl_window = nullptr; SDL_Renderer* sdl_renderer = nullptr; SDL_Joystick *sdl_joystick = nullptr; From 3a33aca47874e1de40d77a221278020f63ab8fb1 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 8 May 2026 18:45:18 +0200 Subject: [PATCH 07/17] SDL: Resolve int division in float context warning --- src/platform/sdl/sdl2_ui.cpp | 4 ++-- src/platform/sdl/sdl3_ui.cpp | 4 ++-- src/platform/sdl/sdl3_ui.h | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index ac9280c1eb..921cf2547a 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -637,9 +637,9 @@ void Sdl2Ui::UpdateDisplay() { if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Integer) { // Integer division on purpose if (want_aspect > real_aspect) { - window.scale = static_cast(win_width / main_surface->width()); + window.scale = static_cast(static_cast(win_width / main_surface->width())); } else { - window.scale = static_cast(win_height / main_surface->height()); + window.scale = static_cast(static_cast(win_height / main_surface->height())); } viewport.w = static_cast(ceilf(main_surface->width() * window.scale)); diff --git a/src/platform/sdl/sdl3_ui.cpp b/src/platform/sdl/sdl3_ui.cpp index 3a284bd911..951771d831 100644 --- a/src/platform/sdl/sdl3_ui.cpp +++ b/src/platform/sdl/sdl3_ui.cpp @@ -614,9 +614,9 @@ void Sdl3Ui::UpdateDisplay() { if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Integer) { // Integer division on purpose if (want_aspect > real_aspect) { - window.scale = static_cast(win_width / main_surface->width()); + window.scale = static_cast(static_cast(win_width / main_surface->width())); } else { - window.scale = static_cast(win_height / main_surface->height()); + window.scale = static_cast(static_cast(win_height / main_surface->height())); } viewport.w = static_cast(ceilf(main_surface->width() * window.scale)); diff --git a/src/platform/sdl/sdl3_ui.h b/src/platform/sdl/sdl3_ui.h index 8f387c1c22..290f8dbfbd 100644 --- a/src/platform/sdl/sdl3_ui.h +++ b/src/platform/sdl/sdl3_ui.h @@ -20,11 +20,9 @@ // Headers #include "baseui.h" -#include "color.h" #include "rect.h" #include "system.h" -#include #include extern "C" { From f1c1335180662307ffb1cb77db2c0f1dcd6bea66 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 8 May 2026 18:55:40 +0200 Subject: [PATCH 08/17] SDL3: Create window with high pixel density support. Otherwise the Window is a blurry mess on 4k displays, at least on Linux --- src/platform/sdl/sdl3_ui.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/platform/sdl/sdl3_ui.cpp b/src/platform/sdl/sdl3_ui.cpp index 951771d831..4ae4c866ee 100644 --- a/src/platform/sdl/sdl3_ui.cpp +++ b/src/platform/sdl/sdl3_ui.cpp @@ -346,7 +346,8 @@ bool Sdl3Ui::RefreshDisplayMode() { // Create our window SDL_PropertiesID wprops = SDL_CreateProperties(); SDL_SetStringProperty(wprops, SDL_PROP_WINDOW_CREATE_TITLE_STRING, GAME_TITLE); - SDL_SetNumberProperty(wprops, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER, SDL_WINDOW_RESIZABLE | flags); + SDL_SetNumberProperty(wprops, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER, + SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | flags); if (vcfg.window_x.Get() < 0 || vcfg.window_y.Get() < 0 || vcfg.window_height.Get() <= 0 || vcfg.window_width.Get() <= 0) { SDL_SetNumberProperty(wprops, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED); @@ -836,9 +837,13 @@ void Sdl3Ui::ProcessMouseMotionEvent(SDL_Event& evnt) { return; } - mouse_pos.x = (evnt.motion.x - viewport.x) * main_surface->width() / xw; - mouse_pos.y = (evnt.motion.y - viewport.y) * main_surface->height() / yh; - + if (SDL_ConvertEventToRenderCoordinates(sdl_renderer, &evnt)) { + mouse_pos.x = evnt.motion.x * main_surface->width() / xw; + mouse_pos.y = evnt.motion.y * main_surface->height() / yh; + } else { + mouse_pos.x = (evnt.motion.x - viewport.x) * main_surface->width() / xw; + mouse_pos.y = (evnt.motion.y - viewport.y) * main_surface->height() / yh; + } #else /* unused */ (void) evnt; From 3e56bc79af5a3048a74a73bc1599f31b1323cc11 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 8 May 2026 20:45:01 +0200 Subject: [PATCH 09/17] Emscripten: Export HEAPU8 to fix download/upload of files after bumping the version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0aec491985..bd96cd28c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1247,7 +1247,7 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N "-sALLOW_MEMORY_GROWTH -sMINIFY_HTML=0 -sMODULARIZE -sEXPORT_NAME=createEasyRpgPlayer \ -sEXIT_RUNTIME --bind --pre-js ${PLAYER_JS_PREJS} --post-js ${PLAYER_JS_POSTJS} \ -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['$autoResumeAudioContext','$dynCall'] \ - -sEXPORTED_RUNTIME_METHODS=['FS'] \ + -sEXPORTED_RUNTIME_METHODS=['FS','HEAPU8'] \ -sEXPORTED_FUNCTIONS=_main,_malloc,_free") set_source_files_properties("src/platform/sdl/main.cpp" PROPERTIES OBJECT_DEPENDS "${PLAYER_JS_PREJS};${PLAYER_JS_POSTJS};${PLAYER_JS_SHELL}") From 260d9d3b7ff73d7aa7d1ba647553ff0240be58cc Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 8 May 2026 20:46:52 +0200 Subject: [PATCH 10/17] Emscripten: Disable file logging and prevent async requests on shutdown which corrupts memory This can happen when the system graphic is invalid, as ResetGameObjects will init the data structures again Another issue with our fragile shutdown system :( --- src/async_handler.cpp | 5 +++++ src/game_config.cpp | 5 +++-- src/player.cpp | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/async_handler.cpp b/src/async_handler.cpp index 7301223510..097416f6dd 100644 --- a/src/async_handler.cpp +++ b/src/async_handler.cpp @@ -298,6 +298,11 @@ void FileRequestAsync::SetGraphicFile(bool graphic) { } void FileRequestAsync::Start() { + if (Player::exit_flag) { + // Ignore requests when the Player is shutting down + return; + } + if (file == CACHE_DEFAULT_BITMAP) { // Embedded asset -> Fire immediately DownloadDone(true); diff --git a/src/game_config.cpp b/src/game_config.cpp index 4f94cb7098..9d530ccbf6 100644 --- a/src/game_config.cpp +++ b/src/game_config.cpp @@ -112,10 +112,11 @@ Game_Config Game_Config::Create(CmdlineParser& cp) { cfg.input.gamepad_swap_ab_and_xy.Set(true); #endif -#if defined(USE_CUSTOM_FILEBUF) || defined(USE_LIBRETRO) - // Disable logging by default on +#if defined(USE_CUSTOM_FILEBUF) || defined(USE_LIBRETRO) || defined(__EMSCRIPTEN__) + // Disable logging to file by default on // - platforms with slow IO or bad FS drivers // - libretro because the frontend handles the logging + // - emscripten because this is written to a temporary FS cfg.player.log_enabled.Set(false); #endif diff --git a/src/player.cpp b/src/player.cpp index 3b414d41b9..1ff38f8c6c 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -407,6 +407,8 @@ int Player::GetFrames() { } void Player::Exit() { + Player::exit_flag = true; + if (player_config.settings_autosave.Get()) { Scene_Settings::SaveConfig(true); } From 13565d080bcda25f838b99204b12e026718db486 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Fri, 8 May 2026 20:48:31 +0200 Subject: [PATCH 11/17] Emscripten: The documentation recommends to not use EXIT_RUNTIME as is unncessary There also seems to be a codegen bug. Invoking of some constructors on shutdown results in "function signature mismatch". This prevents the global constructors to be called at the end. Manually invoke our onExit handler instead. Removed the extern from main as this emitted a warning Fix #3464 --- CMakeLists.txt | 2 +- src/platform/emscripten/main.cpp | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bd96cd28c9..9707ab6d9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1245,7 +1245,7 @@ if(PLAYER_BUILD_EXECUTABLE AND ${PLAYER_TARGET_PLATFORM} MATCHES "^SDL.*$" AND N set_property(TARGET ${EXE_NAME} PROPERTY LINK_FLAGS "-sALLOW_MEMORY_GROWTH -sMINIFY_HTML=0 -sMODULARIZE -sEXPORT_NAME=createEasyRpgPlayer \ - -sEXIT_RUNTIME --bind --pre-js ${PLAYER_JS_PREJS} --post-js ${PLAYER_JS_POSTJS} \ + -sEXIT_RUNTIME=0 --bind --pre-js ${PLAYER_JS_PREJS} --post-js ${PLAYER_JS_POSTJS} \ -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['$autoResumeAudioContext','$dynCall'] \ -sEXPORTED_RUNTIME_METHODS=['FS','HEAPU8'] \ -sEXPORTED_FUNCTIONS=_main,_malloc,_free") diff --git a/src/platform/emscripten/main.cpp b/src/platform/emscripten/main.cpp index dc88edaa55..1424d743bb 100644 --- a/src/platform/emscripten/main.cpp +++ b/src/platform/emscripten/main.cpp @@ -77,6 +77,11 @@ void main_loop() { return; } emscripten_cancel_main_loop(); + EM_ASM({ + if (Module.onExit !== undefined) { + Module.onExit(); + } + }); } } @@ -84,7 +89,7 @@ void main_loop() { * If the main function ever needs to change, be sure to update the `main()` * functions of the other platforms as well. */ -extern "C" int main(int argc, char* argv[]) { +int main(int argc, char* argv[]) { args.assign(argv, argv + argc); Output::IgnorePause(true); From 359496d7cd70b58c561585cb1a4a184cc08347d0 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Thu, 14 May 2026 16:32:10 +0200 Subject: [PATCH 12/17] Android: Add SDL3 support Not really working. The second time a game starts it reports SIGSEGV and I cannot figure out the reason because the debugger does not catch the signal. --- .gitattributes | 2 - .gitignore | 1 + builds/android/app/build.gradle | 33 +- .../game_browser/GameBrowserActivity.java | 2 +- .../player/game_browser/GameScanner.java | 2 +- .../player/player/EasyRpgPlayerActivity.java | 2 +- .../player/settings/SettingsFontActivity.java | 2 +- .../settings/SettingsGamesFolderActivity.java | 2 +- .../main/java/org/libsdl/app/HIDDevice.java | 22 - .../app/HIDDeviceBLESteamController.java | 650 ----- .../java/org/libsdl/app/HIDDeviceManager.java | 698 ------ .../java/org/libsdl/app/HIDDeviceUSB.java | 309 --- .../app/src/main/java/org/libsdl/app/SDL.java | 90 - .../main/java/org/libsdl/app/SDLActivity.java | 2122 ----------------- .../java/org/libsdl/app/SDLAudioManager.java | 514 ---- .../org/libsdl/app/SDLControllerManager.java | 856 ------- .../main/java/org/libsdl/app/SDLSurface.java | 405 ---- builds/android/gradle.properties | 3 + src/filefinder_rtp.cpp | 6 +- src/main_data.cpp | 5 +- src/platform/android/filesystem_apk.cpp | 2 +- src/platform/android/filesystem_saf.cpp | 2 +- src/platform/sdl/main.cpp | 2 +- src/platform/sdl/sdl3_ui.cpp | 4 +- 24 files changed, 49 insertions(+), 5687 deletions(-) delete mode 100644 builds/android/app/src/main/java/org/libsdl/app/HIDDevice.java delete mode 100644 builds/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java delete mode 100644 builds/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java delete mode 100644 builds/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java delete mode 100644 builds/android/app/src/main/java/org/libsdl/app/SDL.java delete mode 100644 builds/android/app/src/main/java/org/libsdl/app/SDLActivity.java delete mode 100644 builds/android/app/src/main/java/org/libsdl/app/SDLAudioManager.java delete mode 100644 builds/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java delete mode 100644 builds/android/app/src/main/java/org/libsdl/app/SDLSurface.java diff --git a/.gitattributes b/.gitattributes index 282be498c8..1e048646e5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,8 +15,6 @@ src/midiprogram.h -diff linguist-vendored src/midisequencer.* -diff linguist-vendored src/midisynth.* -diff linguist-vendored CMakePresets.json -diff linguist-generated -builds/android/app/src/main/java/org/libsdl/app/*.java -diff linguist-vendored -builds/android/app/src/main/java/org/libsdl/app/SDLActivity.java diff src/generated/* -diff linguist-generated .gitattributes export-ignore diff --git a/.gitignore b/.gitignore index 1cc1f3f63c..c620459d13 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,7 @@ __MACOSX # Android *.iml local.properties +*.aar # libretro easyrpg_libretro.* diff --git a/builds/android/app/build.gradle b/builds/android/app/build.gradle index 48dd2db6e2..6ddfcd1b66 100644 --- a/builds/android/app/build.gradle +++ b/builds/android/app/build.gradle @@ -1,8 +1,12 @@ apply plugin: 'com.android.application' + android { namespace "org.easyrpg.player" - ndkVersion '21.4.7075529' + ndkVersion "28.2.13676358" assetPacks = [":assets"] + buildFeatures { + prefab true + } defaultConfig { applicationId "org.easyrpg.player" compileSdkVersion 36 @@ -76,6 +80,7 @@ allprojects { } dependencies { + implementation files("SDL3-${sdlVersion}.aar") implementation 'androidx.appcompat:appcompat:1.7.1' implementation "androidx.activity:activity:1.13.0" implementation 'com.google.android.material:material:1.13.0' @@ -85,3 +90,29 @@ dependencies { implementation 'androidx.documentfile:documentfile:1.1.0' implementation 'org.ini4j:ini4j:0.5.4' } + +afterEvaluate { + preBuild.dependsOn downloadSdlAar +} + +tasks.register('downloadSdlAar') { + doLast { + def sdlAarDest = file("SDL3-${sdlVersion}.aar") + if (!sdlAarDest.exists()) { + println "Downloading SDL ${sdlVersion} AAR..." + def sdlZipDest = layout.buildDirectory.file("sdl3.zip").get().asFile + sdlZipDest.parentFile.mkdirs() + new URL("https://github.com/libsdl-org/SDL/releases/download/release-${sdlVersion}/SDL3-devel-${sdlVersion}-android.zip") + .withInputStream { i -> sdlZipDest.withOutputStream { it << i } } + copy { + from zipTree(sdlZipDest) + into projectDir + include "**/*.aar" + eachFile { FileCopyDetails fcp -> + fcp.relativePath = new RelativePath(true, fcp.file.name) + } + includeEmptyDirs = false + } + } + } +} diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java index 4e2671e3ba..53e5cfa68d 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameBrowserActivity.java @@ -56,7 +56,7 @@ public class GameBrowserActivity extends BaseActivity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - SDL.setContext(getApplicationContext()); + SDL.setContext(this); setContentView(R.layout.activity_games_browser); diff --git a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameScanner.java b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameScanner.java index 687f388d65..0db5e37ec4 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameScanner.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/game_browser/GameScanner.java @@ -138,7 +138,7 @@ private int scanFolderHash(Context context, Uri folderURI) { private void scanRootFolder(Activity activity, Uri folderURI) { Context context = activity.getApplicationContext(); - SDL.setContext(context); + SDL.setContext(activity); final ArrayList names = new ArrayList<>(); final ArrayList fileURIs = new ArrayList<>(); diff --git a/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java index 71973b1d70..4e998e1dc7 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/player/EasyRpgPlayerActivity.java @@ -97,7 +97,7 @@ public class EasyRpgPlayerActivity extends SDLActivity implements NavigationView @Override protected String[] getLibraries() { return new String[] { - "SDL2", + "SDL3", "easyrpg_android" }; } diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java index baa373d9c9..161ba9ac00 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsFontActivity.java @@ -44,7 +44,7 @@ protected void onCreate(Bundle savedInstanceState) { fonts1ListLayout = findViewById(R.id.settings_font1_list); fonts2ListLayout = findViewById(R.id.settings_font2_list); - SDL.setContext(getApplicationContext()); + SDL.setContext(this); // Setup UI components // The Font Button diff --git a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsGamesFolderActivity.java b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsGamesFolderActivity.java index c43ddaacc4..82ff372337 100644 --- a/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsGamesFolderActivity.java +++ b/builds/android/app/src/main/java/org/easyrpg/player/settings/SettingsGamesFolderActivity.java @@ -26,7 +26,7 @@ public class SettingsGamesFolderActivity extends BaseActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(R.layout.activity_settings_easyrpg_folders); - SDL.setContext(getApplicationContext()); + SDL.setContext(this); safError = GameBrowserHelper.SafError.OK; diff --git a/builds/android/app/src/main/java/org/libsdl/app/HIDDevice.java b/builds/android/app/src/main/java/org/libsdl/app/HIDDevice.java deleted file mode 100644 index 955df5d14c..0000000000 --- a/builds/android/app/src/main/java/org/libsdl/app/HIDDevice.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.libsdl.app; - -import android.hardware.usb.UsbDevice; - -interface HIDDevice -{ - public int getId(); - public int getVendorId(); - public int getProductId(); - public String getSerialNumber(); - public int getVersion(); - public String getManufacturerName(); - public String getProductName(); - public UsbDevice getDevice(); - public boolean open(); - public int sendFeatureReport(byte[] report); - public int sendOutputReport(byte[] report); - public boolean getFeatureReport(byte[] report); - public void setFrozen(boolean frozen); - public void close(); - public void shutdown(); -} diff --git a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java deleted file mode 100644 index ee5521fd5e..0000000000 --- a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java +++ /dev/null @@ -1,650 +0,0 @@ -package org.libsdl.app; - -import android.content.Context; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCallback; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattDescriptor; -import android.bluetooth.BluetoothManager; -import android.bluetooth.BluetoothProfile; -import android.bluetooth.BluetoothGattService; -import android.hardware.usb.UsbDevice; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.os.*; - -//import com.android.internal.util.HexDump; - -import java.lang.Runnable; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.UUID; - -class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { - - private static final String TAG = "hidapi"; - private HIDDeviceManager mManager; - private BluetoothDevice mDevice; - private int mDeviceId; - private BluetoothGatt mGatt; - private boolean mIsRegistered = false; - private boolean mIsConnected = false; - private boolean mIsChromebook = false; - private boolean mIsReconnecting = false; - private boolean mFrozen = false; - private LinkedList mOperations; - GattOperation mCurrentOperation = null; - private Handler mHandler; - - private static final int TRANSPORT_AUTO = 0; - private static final int TRANSPORT_BREDR = 1; - private static final int TRANSPORT_LE = 2; - - private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; - - static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); - static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); - static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); - static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; - - static class GattOperation { - private enum Operation { - CHR_READ, - CHR_WRITE, - ENABLE_NOTIFICATION - } - - Operation mOp; - UUID mUuid; - byte[] mValue; - BluetoothGatt mGatt; - boolean mResult = true; - - private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { - mGatt = gatt; - mOp = operation; - mUuid = uuid; - } - - private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { - mGatt = gatt; - mOp = operation; - mUuid = uuid; - mValue = value; - } - - public void run() { - // This is executed in main thread - BluetoothGattCharacteristic chr; - - switch (mOp) { - case CHR_READ: - chr = getCharacteristic(mUuid); - //Log.v(TAG, "Reading characteristic " + chr.getUuid()); - if (!mGatt.readCharacteristic(chr)) { - Log.e(TAG, "Unable to read characteristic " + mUuid.toString()); - mResult = false; - break; - } - mResult = true; - break; - case CHR_WRITE: - chr = getCharacteristic(mUuid); - //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value)); - chr.setValue(mValue); - if (!mGatt.writeCharacteristic(chr)) { - Log.e(TAG, "Unable to write characteristic " + mUuid.toString()); - mResult = false; - break; - } - mResult = true; - break; - case ENABLE_NOTIFICATION: - chr = getCharacteristic(mUuid); - //Log.v(TAG, "Writing descriptor of " + chr.getUuid()); - if (chr != null) { - BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); - if (cccd != null) { - int properties = chr.getProperties(); - byte[] value; - if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) { - value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; - } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) { - value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; - } else { - Log.e(TAG, "Unable to start notifications on input characteristic"); - mResult = false; - return; - } - - mGatt.setCharacteristicNotification(chr, true); - cccd.setValue(value); - if (!mGatt.writeDescriptor(cccd)) { - Log.e(TAG, "Unable to write descriptor " + mUuid.toString()); - mResult = false; - return; - } - mResult = true; - } - } - } - } - - public boolean finish() { - return mResult; - } - - private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { - BluetoothGattService valveService = mGatt.getService(steamControllerService); - if (valveService == null) - return null; - return valveService.getCharacteristic(uuid); - } - - static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) { - return new GattOperation(gatt, Operation.CHR_READ, uuid); - } - - static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) { - return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value); - } - - static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) { - return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid); - } - } - - public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { - mManager = manager; - mDevice = device; - mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier()); - mIsRegistered = false; - mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); - mOperations = new LinkedList(); - mHandler = new Handler(Looper.getMainLooper()); - - mGatt = connectGatt(); - // final HIDDeviceBLESteamController finalThis = this; - // mHandler.postDelayed(new Runnable() { - // @Override - // public void run() { - // finalThis.checkConnectionForChromebookIssue(); - // } - // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); - } - - public String getIdentifier() { - return String.format("SteamController.%s", mDevice.getAddress()); - } - - public BluetoothGatt getGatt() { - return mGatt; - } - - // Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead - // of TRANSPORT_LE. Let's force ourselves to connect low energy. - private BluetoothGatt connectGatt(boolean managed) { - if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) { - try { - return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE); - } catch (Exception e) { - return mDevice.connectGatt(mManager.getContext(), managed, this); - } - } else { - return mDevice.connectGatt(mManager.getContext(), managed, this); - } - } - - private BluetoothGatt connectGatt() { - return connectGatt(false); - } - - protected int getConnectionState() { - - Context context = mManager.getContext(); - if (context == null) { - // We are lacking any context to get our Bluetooth information. We'll just assume disconnected. - return BluetoothProfile.STATE_DISCONNECTED; - } - - BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE); - if (btManager == null) { - // This device doesn't support Bluetooth. We should never be here, because how did - // we instantiate a device to start with? - return BluetoothProfile.STATE_DISCONNECTED; - } - - return btManager.getConnectionState(mDevice, BluetoothProfile.GATT); - } - - public void reconnect() { - - if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) { - mGatt.disconnect(); - mGatt = connectGatt(); - } - - } - - protected void checkConnectionForChromebookIssue() { - if (!mIsChromebook) { - // We only do this on Chromebooks, because otherwise it's really annoying to just attempt - // over and over. - return; - } - - int connectionState = getConnectionState(); - - switch (connectionState) { - case BluetoothProfile.STATE_CONNECTED: - if (!mIsConnected) { - // We are in the Bad Chromebook Place. We can force a disconnect - // to try to recover. - Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect."); - mIsReconnecting = true; - mGatt.disconnect(); - mGatt = connectGatt(false); - break; - } - else if (!isRegistered()) { - if (mGatt.getServices().size() > 0) { - Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover."); - probeService(this); - } - else { - Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover."); - mIsReconnecting = true; - mGatt.disconnect(); - mGatt = connectGatt(false); - break; - } - } - else { - Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!"); - return; - } - break; - - case BluetoothProfile.STATE_DISCONNECTED: - Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover."); - - mIsReconnecting = true; - mGatt.disconnect(); - mGatt = connectGatt(false); - break; - - case BluetoothProfile.STATE_CONNECTING: - Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer."); - break; - } - - final HIDDeviceBLESteamController finalThis = this; - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - finalThis.checkConnectionForChromebookIssue(); - } - }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); - } - - private boolean isRegistered() { - return mIsRegistered; - } - - private void setRegistered() { - mIsRegistered = true; - } - - private boolean probeService(HIDDeviceBLESteamController controller) { - - if (isRegistered()) { - return true; - } - - if (!mIsConnected) { - return false; - } - - Log.v(TAG, "probeService controller=" + controller); - - for (BluetoothGattService service : mGatt.getServices()) { - if (service.getUuid().equals(steamControllerService)) { - Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); - - for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { - if (chr.getUuid().equals(inputCharacteristic)) { - Log.v(TAG, "Found input characteristic"); - // Start notifications - BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); - if (cccd != null) { - enableNotification(chr.getUuid()); - } - } - } - return true; - } - } - - if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) { - Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us."); - mIsConnected = false; - mIsReconnecting = true; - mGatt.disconnect(); - mGatt = connectGatt(false); - } - - return false; - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void finishCurrentGattOperation() { - GattOperation op = null; - synchronized (mOperations) { - if (mCurrentOperation != null) { - op = mCurrentOperation; - mCurrentOperation = null; - } - } - if (op != null) { - boolean result = op.finish(); // TODO: Maybe in main thread as well? - - // Our operation failed, let's add it back to the beginning of our queue. - if (!result) { - mOperations.addFirst(op); - } - } - executeNextGattOperation(); - } - - private void executeNextGattOperation() { - synchronized (mOperations) { - if (mCurrentOperation != null) - return; - - if (mOperations.isEmpty()) - return; - - mCurrentOperation = mOperations.removeFirst(); - } - - // Run in main thread - mHandler.post(new Runnable() { - @Override - public void run() { - synchronized (mOperations) { - if (mCurrentOperation == null) { - Log.e(TAG, "Current operation null in executor?"); - return; - } - - mCurrentOperation.run(); - // now wait for the GATT callback and when it comes, finish this operation - } - } - }); - } - - private void queueGattOperation(GattOperation op) { - synchronized (mOperations) { - mOperations.add(op); - } - executeNextGattOperation(); - } - - private void enableNotification(UUID chrUuid) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid); - queueGattOperation(op); - } - - public void writeCharacteristic(UUID uuid, byte[] value) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value); - queueGattOperation(op); - } - - public void readCharacteristic(UUID uuid) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid); - queueGattOperation(op); - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////// BluetoothGattCallback overridden methods - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - public void onConnectionStateChange(BluetoothGatt g, int status, int newState) { - //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState); - mIsReconnecting = false; - if (newState == 2) { - mIsConnected = true; - // Run directly, without GattOperation - if (!isRegistered()) { - mHandler.post(new Runnable() { - @Override - public void run() { - mGatt.discoverServices(); - } - }); - } - } - else if (newState == 0) { - mIsConnected = false; - } - - // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent. - } - - public void onServicesDiscovered(BluetoothGatt gatt, int status) { - //Log.v(TAG, "onServicesDiscovered status=" + status); - if (status == 0) { - if (gatt.getServices().size() == 0) { - Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack."); - mIsReconnecting = true; - mIsConnected = false; - gatt.disconnect(); - mGatt = connectGatt(false); - } - else { - probeService(this); - } - } - } - - public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid()); - - if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) { - mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue()); - } - - finishCurrentGattOperation(); - } - - public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid()); - - if (characteristic.getUuid().equals(reportCharacteristic)) { - // Only register controller with the native side once it has been fully configured - if (!isRegistered()) { - Log.v(TAG, "Registering Steam Controller with ID: " + getId()); - mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0); - setRegistered(); - } - } - - finishCurrentGattOperation(); - } - - public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - // Enable this for verbose logging of controller input reports - //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue())); - - if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { - mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); - } - } - - public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - //Log.v(TAG, "onDescriptorRead status=" + status); - } - - public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); - //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); - - if (chr.getUuid().equals(inputCharacteristic)) { - boolean hasWrittenInputDescriptor = true; - BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); - if (reportChr != null) { - Log.v(TAG, "Writing report characteristic to enter valve mode"); - reportChr.setValue(enterValveMode); - gatt.writeCharacteristic(reportChr); - } - } - - finishCurrentGattOperation(); - } - - public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { - //Log.v(TAG, "onReliableWriteCompleted status=" + status); - } - - public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { - //Log.v(TAG, "onReadRemoteRssi status=" + status); - } - - public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { - //Log.v(TAG, "onMtuChanged status=" + status); - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - //////// Public API - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public int getId() { - return mDeviceId; - } - - @Override - public int getVendorId() { - // Valve Corporation - final int VALVE_USB_VID = 0x28DE; - return VALVE_USB_VID; - } - - @Override - public int getProductId() { - // We don't have an easy way to query from the Bluetooth device, but we know what it is - final int D0G_BLE2_PID = 0x1106; - return D0G_BLE2_PID; - } - - @Override - public String getSerialNumber() { - // This will be read later via feature report by Steam - return "12345"; - } - - @Override - public int getVersion() { - return 0; - } - - @Override - public String getManufacturerName() { - return "Valve Corporation"; - } - - @Override - public String getProductName() { - return "Steam Controller"; - } - - @Override - public UsbDevice getDevice() { - return null; - } - - @Override - public boolean open() { - return true; - } - - @Override - public int sendFeatureReport(byte[] report) { - if (!isRegistered()) { - Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!"); - if (mIsConnected) { - probeService(this); - } - return -1; - } - - // We need to skip the first byte, as that doesn't go over the air - byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); - //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report)); - writeCharacteristic(reportCharacteristic, actual_report); - return report.length; - } - - @Override - public int sendOutputReport(byte[] report) { - if (!isRegistered()) { - Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!"); - if (mIsConnected) { - probeService(this); - } - return -1; - } - - //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report)); - writeCharacteristic(reportCharacteristic, report); - return report.length; - } - - @Override - public boolean getFeatureReport(byte[] report) { - if (!isRegistered()) { - Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!"); - if (mIsConnected) { - probeService(this); - } - return false; - } - - //Log.v(TAG, "getFeatureReport"); - readCharacteristic(reportCharacteristic); - return true; - } - - @Override - public void close() { - } - - @Override - public void setFrozen(boolean frozen) { - mFrozen = frozen; - } - - @Override - public void shutdown() { - close(); - - BluetoothGatt g = mGatt; - if (g != null) { - g.disconnect(); - g.close(); - mGatt = null; - } - mManager = null; - mIsRegistered = false; - mIsConnected = false; - mOperations.clear(); - } - -} - diff --git a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java deleted file mode 100644 index 21a1c1d18e..0000000000 --- a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ /dev/null @@ -1,698 +0,0 @@ -package org.libsdl.app; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.PendingIntent; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.bluetooth.BluetoothProfile; -import android.os.Build; -import android.util.Log; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.hardware.usb.*; -import android.os.Handler; -import android.os.Looper; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; - -public class HIDDeviceManager { - private static final String TAG = "hidapi"; - private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION"; - - private static HIDDeviceManager sManager; - private static int sManagerRefCount = 0; - - public static HIDDeviceManager acquire(Context context) { - if (sManagerRefCount == 0) { - sManager = new HIDDeviceManager(context); - } - ++sManagerRefCount; - return sManager; - } - - public static void release(HIDDeviceManager manager) { - if (manager == sManager) { - --sManagerRefCount; - if (sManagerRefCount == 0) { - sManager.close(); - sManager = null; - } - } - } - - private Context mContext; - private HashMap mDevicesById = new HashMap(); - private HashMap mBluetoothDevices = new HashMap(); - private int mNextDeviceId = 0; - private SharedPreferences mSharedPreferences = null; - private boolean mIsChromebook = false; - private UsbManager mUsbManager; - private Handler mHandler; - private BluetoothManager mBluetoothManager; - private List mLastBluetoothDevices; - - private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { - UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); - handleUsbDeviceAttached(usbDevice); - } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { - UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); - handleUsbDeviceDetached(usbDevice); - } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) { - UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); - handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); - } - } - }; - - private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - // Bluetooth device was connected. If it was a Steam Controller, handle it - if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - Log.d(TAG, "Bluetooth device connected: " + device); - - if (isSteamController(device)) { - connectBluetoothDevice(device); - } - } - - // Bluetooth device was disconnected, remove from controller manager (if any) - if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - Log.d(TAG, "Bluetooth device disconnected: " + device); - - disconnectBluetoothDevice(device); - } - } - }; - - private HIDDeviceManager(final Context context) { - mContext = context; - - HIDDeviceRegisterCallback(); - - mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE); - mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); - -// if (shouldClear) { -// SharedPreferences.Editor spedit = mSharedPreferences.edit(); -// spedit.clear(); -// spedit.commit(); -// } -// else - { - mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); - } - } - - public Context getContext() { - return mContext; - } - - public int getDeviceIDForIdentifier(String identifier) { - SharedPreferences.Editor spedit = mSharedPreferences.edit(); - - int result = mSharedPreferences.getInt(identifier, 0); - if (result == 0) { - result = mNextDeviceId++; - spedit.putInt("next_device_id", mNextDeviceId); - } - - spedit.putInt(identifier, result); - spedit.commit(); - return result; - } - - private void initializeUSB() { - mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE); - if (mUsbManager == null) { - return; - } - - /* - // Logging - for (UsbDevice device : mUsbManager.getDeviceList().values()) { - Log.i(TAG,"Path: " + device.getDeviceName()); - Log.i(TAG,"Manufacturer: " + device.getManufacturerName()); - Log.i(TAG,"Product: " + device.getProductName()); - Log.i(TAG,"ID: " + device.getDeviceId()); - Log.i(TAG,"Class: " + device.getDeviceClass()); - Log.i(TAG,"Protocol: " + device.getDeviceProtocol()); - Log.i(TAG,"Vendor ID " + device.getVendorId()); - Log.i(TAG,"Product ID: " + device.getProductId()); - Log.i(TAG,"Interface count: " + device.getInterfaceCount()); - Log.i(TAG,"---------------------------------------"); - - // Get interface details - for (int index = 0; index < device.getInterfaceCount(); index++) { - UsbInterface mUsbInterface = device.getInterface(index); - Log.i(TAG," ***** *****"); - Log.i(TAG," Interface index: " + index); - Log.i(TAG," Interface ID: " + mUsbInterface.getId()); - Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass()); - Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass()); - Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol()); - Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount()); - - // Get endpoint details - for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++) - { - UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi); - Log.i(TAG," ++++ ++++ ++++"); - Log.i(TAG," Endpoint index: " + epi); - Log.i(TAG," Attributes: " + mEndpoint.getAttributes()); - Log.i(TAG," Direction: " + mEndpoint.getDirection()); - Log.i(TAG," Number: " + mEndpoint.getEndpointNumber()); - Log.i(TAG," Interval: " + mEndpoint.getInterval()); - Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize()); - Log.i(TAG," Type: " + mEndpoint.getType()); - } - } - } - Log.i(TAG," No more devices connected."); - */ - - // Register for USB broadcasts and permission completions - IntentFilter filter = new IntentFilter(); - filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); - filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); - filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); - mContext.registerReceiver(mUsbBroadcast, filter); - - for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { - handleUsbDeviceAttached(usbDevice); - } - } - - UsbManager getUSBManager() { - return mUsbManager; - } - - private void shutdownUSB() { - try { - mContext.unregisterReceiver(mUsbBroadcast); - } catch (Exception e) { - // We may not have registered, that's okay - } - } - - private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) { - if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { - return true; - } - if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) { - return true; - } - return false; - } - - private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) { - final int XB360_IFACE_SUBCLASS = 93; - final int XB360_IFACE_PROTOCOL = 1; // Wired - final int XB360W_IFACE_PROTOCOL = 129; // Wireless - final int[] SUPPORTED_VENDORS = { - 0x0079, // GPD Win 2 - 0x044f, // Thrustmaster - 0x045e, // Microsoft - 0x046d, // Logitech - 0x056e, // Elecom - 0x06a3, // Saitek - 0x0738, // Mad Catz - 0x07ff, // Mad Catz - 0x0e6f, // PDP - 0x0f0d, // Hori - 0x1038, // SteelSeries - 0x11c9, // Nacon - 0x12ab, // Unknown - 0x1430, // RedOctane - 0x146b, // BigBen - 0x1532, // Razer Sabertooth - 0x15e4, // Numark - 0x162e, // Joytech - 0x1689, // Razer Onza - 0x1949, // Lab126, Inc. - 0x1bad, // Harmonix - 0x20d6, // PowerA - 0x24c6, // PowerA - 0x2c22, // Qanba - 0x2dc8, // 8BitDo - 0x9886, // ASTRO Gaming - }; - - if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && - usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS && - (usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL || - usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) { - int vendor_id = usbDevice.getVendorId(); - for (int supportedVid : SUPPORTED_VENDORS) { - if (vendor_id == supportedVid) { - return true; - } - } - } - return false; - } - - private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) { - final int XB1_IFACE_SUBCLASS = 71; - final int XB1_IFACE_PROTOCOL = 208; - final int[] SUPPORTED_VENDORS = { - 0x03f0, // HP - 0x044f, // Thrustmaster - 0x045e, // Microsoft - 0x0738, // Mad Catz - 0x0b05, // ASUS - 0x0e6f, // PDP - 0x0f0d, // Hori - 0x10f5, // Turtle Beach - 0x1532, // Razer Wildcat - 0x20d6, // PowerA - 0x24c6, // PowerA - 0x2dc8, // 8BitDo - 0x2e24, // Hyperkin - 0x3537, // GameSir - }; - - if (usbInterface.getId() == 0 && - usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && - usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS && - usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) { - int vendor_id = usbDevice.getVendorId(); - for (int supportedVid : SUPPORTED_VENDORS) { - if (vendor_id == supportedVid) { - return true; - } - } - } - return false; - } - - private void handleUsbDeviceAttached(UsbDevice usbDevice) { - connectHIDDeviceUSB(usbDevice); - } - - private void handleUsbDeviceDetached(UsbDevice usbDevice) { - List devices = new ArrayList(); - for (HIDDevice device : mDevicesById.values()) { - if (usbDevice.equals(device.getDevice())) { - devices.add(device.getId()); - } - } - for (int id : devices) { - HIDDevice device = mDevicesById.get(id); - mDevicesById.remove(id); - device.shutdown(); - HIDDeviceDisconnected(id); - } - } - - private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) { - for (HIDDevice device : mDevicesById.values()) { - if (usbDevice.equals(device.getDevice())) { - boolean opened = false; - if (permission_granted) { - opened = device.open(); - } - HIDDeviceOpenResult(device.getId(), opened); - } - } - } - - private void connectHIDDeviceUSB(UsbDevice usbDevice) { - synchronized (this) { - int interface_mask = 0; - for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) { - UsbInterface usbInterface = usbDevice.getInterface(interface_index); - if (isHIDDeviceInterface(usbDevice, usbInterface)) { - // Check to see if we've already added this interface - // This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive - int interface_id = usbInterface.getId(); - if ((interface_mask & (1 << interface_id)) != 0) { - continue; - } - interface_mask |= (1 << interface_id); - - HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); - int id = device.getId(); - mDevicesById.put(id, device); - HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol()); - } - } - } - } - - private void initializeBluetooth() { - Log.d(TAG, "Initializing Bluetooth"); - - if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ && - mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT"); - return; - } - - if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ && - mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH"); - return; - } - - if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) { - Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE"); - return; - } - - // Find bonded bluetooth controllers and create SteamControllers for them - mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE); - if (mBluetoothManager == null) { - // This device doesn't support Bluetooth. - return; - } - - BluetoothAdapter btAdapter = mBluetoothManager.getAdapter(); - if (btAdapter == null) { - // This device has Bluetooth support in the codebase, but has no available adapters. - return; - } - - // Get our bonded devices. - for (BluetoothDevice device : btAdapter.getBondedDevices()) { - - Log.d(TAG, "Bluetooth device available: " + device); - if (isSteamController(device)) { - connectBluetoothDevice(device); - } - - } - - // NOTE: These don't work on Chromebooks, to my undying dismay. - IntentFilter filter = new IntentFilter(); - filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); - filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); - mContext.registerReceiver(mBluetoothBroadcast, filter); - - if (mIsChromebook) { - mHandler = new Handler(Looper.getMainLooper()); - mLastBluetoothDevices = new ArrayList(); - - // final HIDDeviceManager finalThis = this; - // mHandler.postDelayed(new Runnable() { - // @Override - // public void run() { - // finalThis.chromebookConnectionHandler(); - // } - // }, 5000); - } - } - - private void shutdownBluetooth() { - try { - mContext.unregisterReceiver(mBluetoothBroadcast); - } catch (Exception e) { - // We may not have registered, that's okay - } - } - - // Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly. - // This function provides a sort of dummy version of that, watching for changes in the - // connected devices and attempting to add controllers as things change. - public void chromebookConnectionHandler() { - if (!mIsChromebook) { - return; - } - - ArrayList disconnected = new ArrayList(); - ArrayList connected = new ArrayList(); - - List currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT); - - for (BluetoothDevice bluetoothDevice : currentConnected) { - if (!mLastBluetoothDevices.contains(bluetoothDevice)) { - connected.add(bluetoothDevice); - } - } - for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) { - if (!currentConnected.contains(bluetoothDevice)) { - disconnected.add(bluetoothDevice); - } - } - - mLastBluetoothDevices = currentConnected; - - for (BluetoothDevice bluetoothDevice : disconnected) { - disconnectBluetoothDevice(bluetoothDevice); - } - for (BluetoothDevice bluetoothDevice : connected) { - connectBluetoothDevice(bluetoothDevice); - } - - final HIDDeviceManager finalThis = this; - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - finalThis.chromebookConnectionHandler(); - } - }, 10000); - } - - public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) { - Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice); - synchronized (this) { - if (mBluetoothDevices.containsKey(bluetoothDevice)) { - Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect"); - - HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); - device.reconnect(); - - return false; - } - HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice); - int id = device.getId(); - mBluetoothDevices.put(bluetoothDevice, device); - mDevicesById.put(id, device); - - // The Steam Controller will mark itself connected once initialization is complete - } - return true; - } - - public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) { - synchronized (this) { - HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); - if (device == null) - return; - - int id = device.getId(); - mBluetoothDevices.remove(bluetoothDevice); - mDevicesById.remove(id); - device.shutdown(); - HIDDeviceDisconnected(id); - } - } - - public boolean isSteamController(BluetoothDevice bluetoothDevice) { - // Sanity check. If you pass in a null device, by definition it is never a Steam Controller. - if (bluetoothDevice == null) { - return false; - } - - // If the device has no local name, we really don't want to try an equality check against it. - if (bluetoothDevice.getName() == null) { - return false; - } - - return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); - } - - private void close() { - shutdownUSB(); - shutdownBluetooth(); - synchronized (this) { - for (HIDDevice device : mDevicesById.values()) { - device.shutdown(); - } - mDevicesById.clear(); - mBluetoothDevices.clear(); - HIDDeviceReleaseCallback(); - } - } - - public void setFrozen(boolean frozen) { - synchronized (this) { - for (HIDDevice device : mDevicesById.values()) { - device.setFrozen(frozen); - } - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - private HIDDevice getDevice(int id) { - synchronized (this) { - HIDDevice result = mDevicesById.get(id); - if (result == null) { - Log.v(TAG, "No device for id: " + id); - Log.v(TAG, "Available devices: " + mDevicesById.keySet()); - } - return result; - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////// JNI interface functions - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - public boolean initialize(boolean usb, boolean bluetooth) { - Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")"); - - if (usb) { - initializeUSB(); - } - if (bluetooth) { - initializeBluetooth(); - } - return true; - } - - public boolean openDevice(int deviceID) { - Log.v(TAG, "openDevice deviceID=" + deviceID); - HIDDevice device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return false; - } - - // Look to see if this is a USB device and we have permission to access it - UsbDevice usbDevice = device.getDevice(); - if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) { - HIDDeviceOpenPending(deviceID); - try { - final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31 - int flags; - if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { - flags = FLAG_MUTABLE; - } else { - flags = 0; - } - if (Build.VERSION.SDK_INT >= 33 /* Android 14.0 (U) */) { - Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION); - intent.setPackage(mContext.getPackageName()); - mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags)); - } else { - mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags)); - } - } catch (Exception e) { - Log.v(TAG, "Couldn't request permission for USB device " + usbDevice); - HIDDeviceOpenResult(deviceID, false); - } - return false; - } - - try { - return device.open(); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return false; - } - - public int sendOutputReport(int deviceID, byte[] report) { - try { - //Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return -1; - } - - return device.sendOutputReport(report); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return -1; - } - - public int sendFeatureReport(int deviceID, byte[] report) { - try { - //Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return -1; - } - - return device.sendFeatureReport(report); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return -1; - } - - public boolean getFeatureReport(int deviceID, byte[] report) { - try { - //Log.v(TAG, "getFeatureReport deviceID=" + deviceID); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return false; - } - - return device.getFeatureReport(report); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return false; - } - - public void closeDevice(int deviceID) { - try { - Log.v(TAG, "closeDevice deviceID=" + deviceID); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return; - } - - device.close(); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - } - - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - /////////////// Native methods - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - private native void HIDDeviceRegisterCallback(); - private native void HIDDeviceReleaseCallback(); - - native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol); - native void HIDDeviceOpenPending(int deviceID); - native void HIDDeviceOpenResult(int deviceID, boolean opened); - native void HIDDeviceDisconnected(int deviceID); - - native void HIDDeviceInputReport(int deviceID, byte[] report); - native void HIDDeviceFeatureReport(int deviceID, byte[] report); -} diff --git a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java deleted file mode 100644 index bfe0cf954d..0000000000 --- a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java +++ /dev/null @@ -1,309 +0,0 @@ -package org.libsdl.app; - -import android.hardware.usb.*; -import android.os.Build; -import android.util.Log; -import java.util.Arrays; - -class HIDDeviceUSB implements HIDDevice { - - private static final String TAG = "hidapi"; - - protected HIDDeviceManager mManager; - protected UsbDevice mDevice; - protected int mInterfaceIndex; - protected int mInterface; - protected int mDeviceId; - protected UsbDeviceConnection mConnection; - protected UsbEndpoint mInputEndpoint; - protected UsbEndpoint mOutputEndpoint; - protected InputThread mInputThread; - protected boolean mRunning; - protected boolean mFrozen; - - public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) { - mManager = manager; - mDevice = usbDevice; - mInterfaceIndex = interface_index; - mInterface = mDevice.getInterface(mInterfaceIndex).getId(); - mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); - mRunning = false; - } - - public String getIdentifier() { - return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex); - } - - @Override - public int getId() { - return mDeviceId; - } - - @Override - public int getVendorId() { - return mDevice.getVendorId(); - } - - @Override - public int getProductId() { - return mDevice.getProductId(); - } - - @Override - public String getSerialNumber() { - String result = null; - if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { - try { - result = mDevice.getSerialNumber(); - } - catch (SecurityException exception) { - //Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage()); - } - } - if (result == null) { - result = ""; - } - return result; - } - - @Override - public int getVersion() { - return 0; - } - - @Override - public String getManufacturerName() { - String result = null; - if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { - result = mDevice.getManufacturerName(); - } - if (result == null) { - result = String.format("%x", getVendorId()); - } - return result; - } - - @Override - public String getProductName() { - String result = null; - if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { - result = mDevice.getProductName(); - } - if (result == null) { - result = String.format("%x", getProductId()); - } - return result; - } - - @Override - public UsbDevice getDevice() { - return mDevice; - } - - public String getDeviceName() { - return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")"; - } - - @Override - public boolean open() { - mConnection = mManager.getUSBManager().openDevice(mDevice); - if (mConnection == null) { - Log.w(TAG, "Unable to open USB device " + getDeviceName()); - return false; - } - - // Force claim our interface - UsbInterface iface = mDevice.getInterface(mInterfaceIndex); - if (!mConnection.claimInterface(iface, true)) { - Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName()); - close(); - return false; - } - - // Find the endpoints - for (int j = 0; j < iface.getEndpointCount(); j++) { - UsbEndpoint endpt = iface.getEndpoint(j); - switch (endpt.getDirection()) { - case UsbConstants.USB_DIR_IN: - if (mInputEndpoint == null) { - mInputEndpoint = endpt; - } - break; - case UsbConstants.USB_DIR_OUT: - if (mOutputEndpoint == null) { - mOutputEndpoint = endpt; - } - break; - } - } - - // Make sure the required endpoints were present - if (mInputEndpoint == null || mOutputEndpoint == null) { - Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); - close(); - return false; - } - - // Start listening for input - mRunning = true; - mInputThread = new InputThread(); - mInputThread.start(); - - return true; - } - - @Override - public int sendFeatureReport(byte[] report) { - int res = -1; - int offset = 0; - int length = report.length; - boolean skipped_report_id = false; - byte report_number = report[0]; - - if (report_number == 0x0) { - ++offset; - --length; - skipped_report_id = true; - } - - res = mConnection.controlTransfer( - UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT, - 0x09/*HID set_report*/, - (3/*HID feature*/ << 8) | report_number, - mInterface, - report, offset, length, - 1000/*timeout millis*/); - - if (res < 0) { - Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName()); - return -1; - } - - if (skipped_report_id) { - ++length; - } - return length; - } - - @Override - public int sendOutputReport(byte[] report) { - int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); - if (r != report.length) { - Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName()); - } - return r; - } - - @Override - public boolean getFeatureReport(byte[] report) { - int res = -1; - int offset = 0; - int length = report.length; - boolean skipped_report_id = false; - byte report_number = report[0]; - - if (report_number == 0x0) { - /* Offset the return buffer by 1, so that the report ID - will remain in byte 0. */ - ++offset; - --length; - skipped_report_id = true; - } - - res = mConnection.controlTransfer( - UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN, - 0x01/*HID get_report*/, - (3/*HID feature*/ << 8) | report_number, - mInterface, - report, offset, length, - 1000/*timeout millis*/); - - if (res < 0) { - Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName()); - return false; - } - - if (skipped_report_id) { - ++res; - ++length; - } - - byte[] data; - if (res == length) { - data = report; - } else { - data = Arrays.copyOfRange(report, 0, res); - } - mManager.HIDDeviceFeatureReport(mDeviceId, data); - - return true; - } - - @Override - public void close() { - mRunning = false; - if (mInputThread != null) { - while (mInputThread.isAlive()) { - mInputThread.interrupt(); - try { - mInputThread.join(); - } catch (InterruptedException e) { - // Keep trying until we're done - } - } - mInputThread = null; - } - if (mConnection != null) { - UsbInterface iface = mDevice.getInterface(mInterfaceIndex); - mConnection.releaseInterface(iface); - mConnection.close(); - mConnection = null; - } - } - - @Override - public void shutdown() { - close(); - mManager = null; - } - - @Override - public void setFrozen(boolean frozen) { - mFrozen = frozen; - } - - protected class InputThread extends Thread { - @Override - public void run() { - int packetSize = mInputEndpoint.getMaxPacketSize(); - byte[] packet = new byte[packetSize]; - while (mRunning) { - int r; - try - { - r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000); - } - catch (Exception e) - { - Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e); - break; - } - if (r < 0) { - // Could be a timeout or an I/O error - } - if (r > 0) { - byte[] data; - if (r == packetSize) { - data = packet; - } else { - data = Arrays.copyOfRange(packet, 0, r); - } - - if (!mFrozen) { - mManager.HIDDeviceInputReport(mDeviceId, data); - } - } - } - } - } -} diff --git a/builds/android/app/src/main/java/org/libsdl/app/SDL.java b/builds/android/app/src/main/java/org/libsdl/app/SDL.java deleted file mode 100644 index 139be9d151..0000000000 --- a/builds/android/app/src/main/java/org/libsdl/app/SDL.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.libsdl.app; - -import android.content.Context; - -import java.lang.Class; -import java.lang.reflect.Method; - -/** - SDL library initialization -*/ -public class SDL { - - // This function should be called first and sets up the native code - // so it can call into the Java classes - public static void setupJNI() { - SDLActivity.nativeSetupJNI(); - SDLAudioManager.nativeSetupJNI(); - SDLControllerManager.nativeSetupJNI(); - } - - // This function should be called each time the activity is started - public static void initialize() { - setContext(null); - - SDLActivity.initialize(); - SDLAudioManager.initialize(); - SDLControllerManager.initialize(); - } - - // This function stores the current activity (SDL or not) - public static void setContext(Context context) { - SDLAudioManager.setContext(context); - mContext = context; - } - - public static Context getContext() { - return mContext; - } - - public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { - loadLibrary(libraryName, mContext); - } - - public static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException { - - if (libraryName == null) { - throw new NullPointerException("No library name provided."); - } - - try { - // Let's see if we have ReLinker available in the project. This is necessary for - // some projects that have huge numbers of local libraries bundled, and thus may - // trip a bug in Android's native library loader which ReLinker works around. (If - // loadLibrary works properly, ReLinker will simply use the normal Android method - // internally.) - // - // To use ReLinker, just add it as a dependency. For more information, see - // https://github.com/KeepSafe/ReLinker for ReLinker's repository. - // - Class relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker"); - Class relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener"); - Class contextClass = context.getClassLoader().loadClass("android.content.Context"); - Class stringClass = context.getClassLoader().loadClass("java.lang.String"); - - // Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if - // they've changed during updates. - Method forceMethod = relinkClass.getDeclaredMethod("force"); - Object relinkInstance = forceMethod.invoke(null); - Class relinkInstanceClass = relinkInstance.getClass(); - - // Actually load the library! - Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass); - loadMethod.invoke(relinkInstance, context, libraryName, null, null); - } - catch (final Throwable e) { - // Fall back - try { - System.loadLibrary(libraryName); - } - catch (final UnsatisfiedLinkError ule) { - throw ule; - } - catch (final SecurityException se) { - throw se; - } - } - } - - protected static Context mContext; -} diff --git a/builds/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/builds/android/app/src/main/java/org/libsdl/app/SDLActivity.java deleted file mode 100644 index b2fd9cd5ee..0000000000 --- a/builds/android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ /dev/null @@ -1,2122 +0,0 @@ -package org.libsdl.app; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.UiModeManager; -import android.content.ClipboardManager; -import android.content.ClipData; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.hardware.Sensor; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.text.Editable; -import android.text.InputType; -import android.text.Selection; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.SparseArray; -import android.view.Display; -import android.view.Gravity; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.PointerIcon; -import android.view.Surface; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.view.inputmethod.BaseInputConnection; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.Hashtable; -import java.util.Locale; - -// EasyRPG additions -import androidx.appcompat.app.AppCompatActivity; - -/** - SDL Activity -*/ -public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { - private static final String TAG = "SDL"; - private static final int SDL_MAJOR_VERSION = 2; - private static final int SDL_MINOR_VERSION = 32; - private static final int SDL_MICRO_VERSION = 10; -/* - // Display InputType.SOURCE/CLASS of events and devices - // - // SDLActivity.debugSource(device.getSources(), "device[" + device.getName() + "]"); - // SDLActivity.debugSource(event.getSource(), "event"); - public static void debugSource(int sources, String prefix) { - int s = sources; - int s_copy = sources; - String cls = ""; - String src = ""; - int tst = 0; - int FLAG_TAINTED = 0x80000000; - - if ((s & InputDevice.SOURCE_CLASS_BUTTON) != 0) cls += " BUTTON"; - if ((s & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) cls += " JOYSTICK"; - if ((s & InputDevice.SOURCE_CLASS_POINTER) != 0) cls += " POINTER"; - if ((s & InputDevice.SOURCE_CLASS_POSITION) != 0) cls += " POSITION"; - if ((s & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) cls += " TRACKBALL"; - - - int s2 = s_copy & ~InputDevice.SOURCE_ANY; // keep class bits - s2 &= ~( InputDevice.SOURCE_CLASS_BUTTON - | InputDevice.SOURCE_CLASS_JOYSTICK - | InputDevice.SOURCE_CLASS_POINTER - | InputDevice.SOURCE_CLASS_POSITION - | InputDevice.SOURCE_CLASS_TRACKBALL); - - if (s2 != 0) cls += "Some_Unknown"; - - s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class; - - if (Build.VERSION.SDK_INT >= 23) { - tst = InputDevice.SOURCE_BLUETOOTH_STYLUS; - if ((s & tst) == tst) src += " BLUETOOTH_STYLUS"; - s2 &= ~tst; - } - - tst = InputDevice.SOURCE_DPAD; - if ((s & tst) == tst) src += " DPAD"; - s2 &= ~tst; - - tst = InputDevice.SOURCE_GAMEPAD; - if ((s & tst) == tst) src += " GAMEPAD"; - s2 &= ~tst; - - if (Build.VERSION.SDK_INT >= 21) { - tst = InputDevice.SOURCE_HDMI; - if ((s & tst) == tst) src += " HDMI"; - s2 &= ~tst; - } - - tst = InputDevice.SOURCE_JOYSTICK; - if ((s & tst) == tst) src += " JOYSTICK"; - s2 &= ~tst; - - tst = InputDevice.SOURCE_KEYBOARD; - if ((s & tst) == tst) src += " KEYBOARD"; - s2 &= ~tst; - - tst = InputDevice.SOURCE_MOUSE; - if ((s & tst) == tst) src += " MOUSE"; - s2 &= ~tst; - - if (Build.VERSION.SDK_INT >= 26) { - tst = InputDevice.SOURCE_MOUSE_RELATIVE; - if ((s & tst) == tst) src += " MOUSE_RELATIVE"; - s2 &= ~tst; - - tst = InputDevice.SOURCE_ROTARY_ENCODER; - if ((s & tst) == tst) src += " ROTARY_ENCODER"; - s2 &= ~tst; - } - tst = InputDevice.SOURCE_STYLUS; - if ((s & tst) == tst) src += " STYLUS"; - s2 &= ~tst; - - tst = InputDevice.SOURCE_TOUCHPAD; - if ((s & tst) == tst) src += " TOUCHPAD"; - s2 &= ~tst; - - tst = InputDevice.SOURCE_TOUCHSCREEN; - if ((s & tst) == tst) src += " TOUCHSCREEN"; - s2 &= ~tst; - - if (Build.VERSION.SDK_INT >= 18) { - tst = InputDevice.SOURCE_TOUCH_NAVIGATION; - if ((s & tst) == tst) src += " TOUCH_NAVIGATION"; - s2 &= ~tst; - } - - tst = InputDevice.SOURCE_TRACKBALL; - if ((s & tst) == tst) src += " TRACKBALL"; - s2 &= ~tst; - - tst = InputDevice.SOURCE_ANY; - if ((s & tst) == tst) src += " ANY"; - s2 &= ~tst; - - if (s == FLAG_TAINTED) src += " FLAG_TAINTED"; - s2 &= ~FLAG_TAINTED; - - if (s2 != 0) src += " Some_Unknown"; - - Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src); - } -*/ - - public static boolean mIsResumedCalled, mHasFocus; - public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */); - - // Cursor types - // private static final int SDL_SYSTEM_CURSOR_NONE = -1; - private static final int SDL_SYSTEM_CURSOR_ARROW = 0; - private static final int SDL_SYSTEM_CURSOR_IBEAM = 1; - private static final int SDL_SYSTEM_CURSOR_WAIT = 2; - private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3; - private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4; - private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5; - private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6; - private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7; - private static final int SDL_SYSTEM_CURSOR_SIZENS = 8; - private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9; - private static final int SDL_SYSTEM_CURSOR_NO = 10; - private static final int SDL_SYSTEM_CURSOR_HAND = 11; - - protected static final int SDL_ORIENTATION_UNKNOWN = 0; - protected static final int SDL_ORIENTATION_LANDSCAPE = 1; - protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2; - protected static final int SDL_ORIENTATION_PORTRAIT = 3; - protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; - - protected static int mCurrentOrientation; - protected static Locale mCurrentLocale; - - // Handle the state of the native layer - public enum NativeState { - INIT, RESUMED, PAUSED - } - - public static NativeState mNextNativeState; - public static NativeState mCurrentNativeState; - - /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ - public static boolean mBrokenLibraries = true; - - // Main components - protected static SDLActivity mSingleton; - protected static SDLSurface mSurface; - protected static DummyEdit mTextEdit; - protected static boolean mScreenKeyboardShown; - protected static ViewGroup mLayout; - protected static SDLClipboardHandler mClipboardHandler; - protected static Hashtable mCursors; - protected static int mLastCursorID; - protected static SDLGenericMotionListener_API12 mMotionListener; - protected static HIDDeviceManager mHIDDeviceManager; - - // This is what SDL runs in. It invokes SDL_main(), eventually - protected static Thread mSDLThread; - - protected static SDLGenericMotionListener_API12 getMotionListener() { - if (mMotionListener == null) { - if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { - mMotionListener = new SDLGenericMotionListener_API26(); - } else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { - mMotionListener = new SDLGenericMotionListener_API24(); - } else { - mMotionListener = new SDLGenericMotionListener_API12(); - } - } - - return mMotionListener; - } - - /** - * This method returns the name of the shared object with the application entry point - * It can be overridden by derived classes. - */ - protected String getMainSharedObject() { - String library; - String[] libraries = SDLActivity.mSingleton.getLibraries(); - if (libraries.length > 0) { - library = "lib" + libraries[libraries.length - 1] + ".so"; - } else { - library = "libmain.so"; - } - return getContext().getApplicationInfo().nativeLibraryDir + "/" + library; - } - - /** - * This method returns the name of the application entry point - * It can be overridden by derived classes. - */ - protected String getMainFunction() { - return "SDL_main"; - } - - /** - * This method is called by SDL before loading the native shared libraries. - * It can be overridden to provide names of shared libraries to be loaded. - * The default implementation returns the defaults. It never returns null. - * An array returned by a new implementation must at least contain "SDL2". - * Also keep in mind that the order the libraries are loaded may matter. - * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). - */ - protected String[] getLibraries() { - return new String[] { - "SDL2", - // "SDL2_image", - // "SDL2_mixer", - // "SDL2_net", - // "SDL2_ttf", - "main" - }; - } - - // Load the .so - public void loadLibraries() { - for (String lib : getLibraries()) { - SDL.loadLibrary(lib, this); - } - } - - /** - * This method is called by SDL before starting the native application thread. - * It can be overridden to provide the arguments after the application name. - * The default implementation returns an empty array. It never returns null. - * @return arguments for the native application. - */ - protected String[] getArguments() { - return new String[0]; - } - - public static void initialize() { - // The static nature of the singleton and Android quirkyness force us to initialize everything here - // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values - mSingleton = null; - mSurface = null; - mTextEdit = null; - mLayout = null; - mClipboardHandler = null; - mCursors = new Hashtable(); - mLastCursorID = 0; - mSDLThread = null; - mIsResumedCalled = false; - mHasFocus = true; - mNextNativeState = NativeState.INIT; - mCurrentNativeState = NativeState.INIT; - } - - protected SDLSurface createSDLSurface(Context context) { - return new SDLSurface(context); - } - - // Setup - @Override - protected void onCreate(Bundle savedInstanceState) { - Log.v(TAG, "Device: " + Build.DEVICE); - Log.v(TAG, "Model: " + Build.MODEL); - Log.v(TAG, "onCreate()"); - super.onCreate(savedInstanceState); - - try { - Thread.currentThread().setName("SDLActivity"); - } catch (Exception e) { - Log.v(TAG, "modify thread properties failed " + e.toString()); - } - - // Load shared libraries - String errorMsgBrokenLib = ""; - try { - loadLibraries(); - mBrokenLibraries = false; /* success */ - } catch(UnsatisfiedLinkError e) { - System.err.println(e.getMessage()); - mBrokenLibraries = true; - errorMsgBrokenLib = e.getMessage(); - } catch(Exception e) { - System.err.println(e.getMessage()); - mBrokenLibraries = true; - errorMsgBrokenLib = e.getMessage(); - } - - if (!mBrokenLibraries) { - String expected_version = String.valueOf(SDL_MAJOR_VERSION) + "." + - String.valueOf(SDL_MINOR_VERSION) + "." + - String.valueOf(SDL_MICRO_VERSION); - String version = nativeGetVersion(); - if (!version.equals(expected_version)) { - mBrokenLibraries = true; - errorMsgBrokenLib = "SDL C/Java version mismatch (expected " + expected_version + ", got " + version + ")"; - } - } - - if (mBrokenLibraries) { - mSingleton = this; - AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); - dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." - + System.getProperty("line.separator") - + System.getProperty("line.separator") - + "Error: " + errorMsgBrokenLib); - dlgAlert.setTitle("SDL Error"); - dlgAlert.setPositiveButton("Exit", - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog,int id) { - // if this button is clicked, close current activity - SDLActivity.mSingleton.finish(); - } - }); - dlgAlert.setCancelable(false); - dlgAlert.create().show(); - - return; - } - - // Set up JNI - SDL.setupJNI(); - - // Initialize state - SDL.initialize(); - - // So we can call stuff from static callbacks - mSingleton = this; - SDL.setContext(this); - - mClipboardHandler = new SDLClipboardHandler(); - - mHIDDeviceManager = HIDDeviceManager.acquire(this); - - // Set up the surface - mSurface = createSDLSurface(this); - - mLayout = new RelativeLayout(this); - mLayout.addView(mSurface); - - // Get our current screen orientation and pass it down. - mCurrentOrientation = SDLActivity.getCurrentOrientation(); - // Only record current orientation - SDLActivity.onNativeOrientationChanged(mCurrentOrientation); - - try { - if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { - mCurrentLocale = getContext().getResources().getConfiguration().locale; - } else { - mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0); - } - } catch(Exception ignored) { - } - - setContentView(mLayout); - - setWindowStyle(false); - - getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); - - // Get filename from "Open with" of another application - Intent intent = getIntent(); - if (intent != null && intent.getData() != null) { - String filename = intent.getData().getPath(); - if (filename != null) { - Log.v(TAG, "Got filename: " + filename); - SDLActivity.onNativeDropFile(filename); - } - } - } - - protected void pauseNativeThread() { - mNextNativeState = NativeState.PAUSED; - mIsResumedCalled = false; - - if (SDLActivity.mBrokenLibraries) { - return; - } - - SDLActivity.handleNativeState(); - } - - protected void resumeNativeThread() { - mNextNativeState = NativeState.RESUMED; - mIsResumedCalled = true; - - if (SDLActivity.mBrokenLibraries) { - return; - } - - SDLActivity.handleNativeState(); - } - - // Events - @Override - protected void onPause() { - Log.v(TAG, "onPause()"); - super.onPause(); - - if (mHIDDeviceManager != null) { - mHIDDeviceManager.setFrozen(true); - } - if (!mHasMultiWindow) { - pauseNativeThread(); - } - } - - @Override - protected void onResume() { - Log.v(TAG, "onResume()"); - super.onResume(); - - if (mHIDDeviceManager != null) { - mHIDDeviceManager.setFrozen(false); - } - if (!mHasMultiWindow) { - resumeNativeThread(); - } - } - - @Override - protected void onStop() { - Log.v(TAG, "onStop()"); - super.onStop(); - if (mHasMultiWindow) { - pauseNativeThread(); - } - } - - @Override - protected void onStart() { - Log.v(TAG, "onStart()"); - super.onStart(); - if (mHasMultiWindow) { - resumeNativeThread(); - } - } - - public static int getCurrentOrientation() { - int result = SDL_ORIENTATION_UNKNOWN; - - Activity activity = (Activity)getContext(); - if (activity == null) { - return result; - } - Display display = activity.getWindowManager().getDefaultDisplay(); - - switch (display.getRotation()) { - case Surface.ROTATION_0: - result = SDL_ORIENTATION_PORTRAIT; - break; - - case Surface.ROTATION_90: - result = SDL_ORIENTATION_LANDSCAPE; - break; - - case Surface.ROTATION_180: - result = SDL_ORIENTATION_PORTRAIT_FLIPPED; - break; - - case Surface.ROTATION_270: - result = SDL_ORIENTATION_LANDSCAPE_FLIPPED; - break; - } - - return result; - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); - - if (SDLActivity.mBrokenLibraries) { - return; - } - - mHasFocus = hasFocus; - if (hasFocus) { - mNextNativeState = NativeState.RESUMED; - SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded(); - - SDLActivity.handleNativeState(); - nativeFocusChanged(true); - - } else { - nativeFocusChanged(false); - if (!mHasMultiWindow) { - mNextNativeState = NativeState.PAUSED; - SDLActivity.handleNativeState(); - } - } - } - - @Override - public void onLowMemory() { - Log.v(TAG, "onLowMemory()"); - super.onLowMemory(); - - if (SDLActivity.mBrokenLibraries) { - return; - } - - SDLActivity.nativeLowMemory(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - Log.v(TAG, "onConfigurationChanged()"); - super.onConfigurationChanged(newConfig); - - if (SDLActivity.mBrokenLibraries) { - return; - } - - if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) { - mCurrentLocale = newConfig.locale; - SDLActivity.onNativeLocaleChanged(); - } - } - - @Override - protected void onDestroy() { - Log.v(TAG, "onDestroy()"); - - if (mHIDDeviceManager != null) { - HIDDeviceManager.release(mHIDDeviceManager); - mHIDDeviceManager = null; - } - - SDLAudioManager.release(this); - - if (SDLActivity.mBrokenLibraries) { - super.onDestroy(); - return; - } - - if (SDLActivity.mSDLThread != null) { - - // Send Quit event to "SDLThread" thread - SDLActivity.nativeSendQuit(); - - // Wait for "SDLThread" thread to end - try { - SDLActivity.mSDLThread.join(); - } catch(Exception e) { - Log.v(TAG, "Problem stopping SDLThread: " + e); - } - } - - SDLActivity.nativeQuit(); - - super.onDestroy(); - } - - @Override - public void onBackPressed() { - // Check if we want to block the back button in case of mouse right click. - // - // If we do, the normal hardware back button will no longer work and people have to use home, - // but the mouse right click will work. - // - boolean trapBack = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_TRAP_BACK_BUTTON", false); - if (trapBack) { - // Exit and let the mouse handler handle this button (if appropriate) - return; - } - - // Default system back button behavior. - if (!isFinishing()) { - super.onBackPressed(); - } - } - - // Called by JNI from SDL. - public static void manualBackButton() { - mSingleton.pressBackButton(); - } - - // Used to get us onto the activity's main thread - public void pressBackButton() { - runOnUiThread(new Runnable() { - @Override - public void run() { - if (!SDLActivity.this.isFinishing()) { - SDLActivity.this.superOnBackPressed(); - } - } - }); - } - - // Used to access the system back behavior. - public void superOnBackPressed() { - super.onBackPressed(); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - - if (SDLActivity.mBrokenLibraries) { - return false; - } - - int keyCode = event.getKeyCode(); - // Ignore certain special keys so they're handled by Android - if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || - keyCode == KeyEvent.KEYCODE_VOLUME_UP || - keyCode == KeyEvent.KEYCODE_CAMERA || - keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */ - keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */ - ) { - return false; - } - return super.dispatchKeyEvent(event); - } - - /* Transition to next state */ - public static void handleNativeState() { - - if (mNextNativeState == mCurrentNativeState) { - // Already in same state, discard. - return; - } - - // Try a transition to init state - if (mNextNativeState == NativeState.INIT) { - - mCurrentNativeState = mNextNativeState; - return; - } - - // Try a transition to paused state - if (mNextNativeState == NativeState.PAUSED) { - if (mSDLThread != null) { - nativePause(); - } - if (mSurface != null) { - mSurface.handlePause(); - } - mCurrentNativeState = mNextNativeState; - return; - } - - // Try a transition to resumed state - if (mNextNativeState == NativeState.RESUMED) { - if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) { - if (mSDLThread == null) { - // This is the entry point to the C app. - // Start up the C app thread and enable sensor input for the first time - // FIXME: Why aren't we enabling sensor input at start? - - mSDLThread = new Thread(new SDLMain(), "SDLThread"); - mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); - mSDLThread.start(); - - // No nativeResume(), don't signal Android_ResumeSem - } else { - nativeResume(); - } - mSurface.handleResume(); - - mCurrentNativeState = mNextNativeState; - } - } - } - - // Messages from the SDLMain thread - static final int COMMAND_CHANGE_TITLE = 1; - static final int COMMAND_CHANGE_WINDOW_STYLE = 2; - static final int COMMAND_TEXTEDIT_HIDE = 3; - static final int COMMAND_SET_KEEP_SCREEN_ON = 5; - - protected static final int COMMAND_USER = 0x8000; - - protected static boolean mFullscreenModeActive; - - /** - * This method is called by SDL if SDL did not handle a message itself. - * This happens if a received message contains an unsupported command. - * Method can be overwritten to handle Messages in a different class. - * @param command the command of the message. - * @param param the parameter of the message. May be null. - * @return if the message was handled in overridden method. - */ - protected boolean onUnhandledMessage(int command, Object param) { - return false; - } - - /** - * A Handler class for Messages from native SDL applications. - * It uses current Activities as target (e.g. for the title). - * static to prevent implicit references to enclosing object. - */ - protected static class SDLCommandHandler extends Handler { - @Override - public void handleMessage(Message msg) { - Context context = SDL.getContext(); - if (context == null) { - Log.e(TAG, "error handling message, getContext() returned null"); - return; - } - switch (msg.arg1) { - case COMMAND_CHANGE_TITLE: - if (context instanceof Activity) { - ((Activity) context).setTitle((String)msg.obj); - } else { - Log.e(TAG, "error handling message, getContext() returned no Activity"); - } - break; - case COMMAND_CHANGE_WINDOW_STYLE: - if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { - if (context instanceof Activity) { - Window window = ((Activity) context).getWindow(); - if (window != null) { - if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { - int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; - window.getDecorView().setSystemUiVisibility(flags); - window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - SDLActivity.mFullscreenModeActive = true; - } else { - int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; - window.getDecorView().setSystemUiVisibility(flags); - window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - SDLActivity.mFullscreenModeActive = false; - } - if (Build.VERSION.SDK_INT >= 28 /* Android 9 (Pie) */) { - window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - } - } - } else { - Log.e(TAG, "error handling message, getContext() returned no Activity"); - } - } - break; - case COMMAND_TEXTEDIT_HIDE: - if (mTextEdit != null) { - // Note: On some devices setting view to GONE creates a flicker in landscape. - // Setting the View's sizes to 0 is similar to GONE but without the flicker. - // The sizes will be set to useful values when the keyboard is shown again. - mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); - - InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); - - mScreenKeyboardShown = false; - - mSurface.requestFocus(); - } - break; - case COMMAND_SET_KEEP_SCREEN_ON: - { - if (context instanceof Activity) { - Window window = ((Activity) context).getWindow(); - if (window != null) { - if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } - } - break; - } - default: - if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { - Log.e(TAG, "error handling message, command is " + msg.arg1); - } - } - } - } - - // Handler for the messages - Handler commandHandler = new SDLCommandHandler(); - - // Send a message from the SDLMain thread - boolean sendCommand(int command, Object data) { - Message msg = commandHandler.obtainMessage(); - msg.arg1 = command; - msg.obj = data; - boolean result = commandHandler.sendMessage(msg); - - if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { - if (command == COMMAND_CHANGE_WINDOW_STYLE) { - // Ensure we don't return until the resize has actually happened, - // or 500ms have passed. - - boolean bShouldWait = false; - - if (data instanceof Integer) { - // Let's figure out if we're already laid out fullscreen or not. - Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - DisplayMetrics realMetrics = new DisplayMetrics(); - display.getRealMetrics(realMetrics); - - boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && - (realMetrics.heightPixels == mSurface.getHeight())); - - if ((Integer) data == 1) { - // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going - // to change size and should wait for surfaceChanged() before we return, so the size - // is right back in native code. If we're already laid out fullscreen, though, we're - // not going to change size even if we change decor modes, so we shouldn't wait for - // surfaceChanged() -- which may not even happen -- and should return immediately. - bShouldWait = !bFullscreenLayout; - } else { - // If we're laid out fullscreen (even if the status bar and nav bar are present), - // or are actively in fullscreen, we're going to change size and should wait for - // surfaceChanged before we return, so the size is right back in native code. - bShouldWait = bFullscreenLayout; - } - } - - if (bShouldWait && (SDLActivity.getContext() != null)) { - // We'll wait for the surfaceChanged() method, which will notify us - // when called. That way, we know our current size is really the - // size we need, instead of grabbing a size that's still got - // the navigation and/or status bars before they're hidden. - // - // We'll wait for up to half a second, because some devices - // take a surprisingly long time for the surface resize, but - // then we'll just give up and return. - // - synchronized (SDLActivity.getContext()) { - try { - SDLActivity.getContext().wait(500); - } catch (InterruptedException ie) { - ie.printStackTrace(); - } - } - } - } - } - - return result; - } - - // C functions we call - public static native String nativeGetVersion(); - public static native int nativeSetupJNI(); - public static native int nativeRunMain(String library, String function, Object arguments); - public static native void nativeLowMemory(); - public static native void nativeSendQuit(); - public static native void nativeQuit(); - public static native void nativePause(); - public static native void nativeResume(); - public static native void nativeFocusChanged(boolean hasFocus); - public static native void onNativeDropFile(String filename); - public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float rate); - public static native void onNativeResize(); - public static native void onNativeKeyDown(int keycode); - public static native void onNativeKeyUp(int keycode); - public static native boolean onNativeSoftReturnKey(); - public static native void onNativeKeyboardFocusLost(); - public static native void onNativeMouse(int button, int action, float x, float y, boolean relative); - public static native void onNativeTouch(int touchDevId, int pointerFingerId, - int action, float x, - float y, float p); - public static native void onNativeAccel(float x, float y, float z); - public static native void onNativeClipboardChanged(); - public static native void onNativeSurfaceCreated(); - public static native void onNativeSurfaceChanged(); - public static native void onNativeSurfaceDestroyed(); - public static native String nativeGetHint(String name); - public static native boolean nativeGetHintBoolean(String name, boolean default_value); - public static native void nativeSetenv(String name, String value); - public static native void onNativeOrientationChanged(int orientation); - public static native void nativeAddTouch(int touchId, String name); - public static native void nativePermissionResult(int requestCode, boolean result); - public static native void onNativeLocaleChanged(); - - /** - * This method is called by SDL using JNI. - */ - public static boolean setActivityTitle(String title) { - // Called from SDLMain() thread and can't directly affect the view - return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); - } - - /** - * This method is called by SDL using JNI. - */ - public static void setWindowStyle(boolean fullscreen) { - // Called from SDLMain() thread and can't directly affect the view - mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0); - } - - /** - * This method is called by SDL using JNI. - * This is a static method for JNI convenience, it calls a non-static method - * so that is can be overridden - */ - public static void setOrientation(int w, int h, boolean resizable, String hint) - { - if (mSingleton != null) { - mSingleton.setOrientationBis(w, h, resizable, hint); - } - } - - /** - * This can be overridden - */ - public void setOrientationBis(int w, int h, boolean resizable, String hint) - { - int orientation_landscape = -1; - int orientation_portrait = -1; - - /* If set, hint "explicitly controls which UI orientations are allowed". */ - if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { - orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; - } else if (hint.contains("LandscapeLeft")) { - orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; - } else if (hint.contains("LandscapeRight")) { - orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; - } - - /* exact match to 'Portrait' to distinguish with PortraitUpsideDown */ - boolean contains_Portrait = hint.contains("Portrait ") || hint.endsWith("Portrait"); - - if (contains_Portrait && hint.contains("PortraitUpsideDown")) { - orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; - } else if (contains_Portrait) { - orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; - } else if (hint.contains("PortraitUpsideDown")) { - orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; - } - - boolean is_landscape_allowed = (orientation_landscape != -1); - boolean is_portrait_allowed = (orientation_portrait != -1); - int req; /* Requested orientation */ - - /* No valid hint, nothing is explicitly allowed */ - if (!is_portrait_allowed && !is_landscape_allowed) { - if (resizable) { - /* All orientations are allowed, respecting user orientation lock setting */ - req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER; - } else { - /* Fixed window and nothing specified. Get orientation from w/h of created window */ - req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); - } - } else { - /* At least one orientation is allowed */ - if (resizable) { - if (is_portrait_allowed && is_landscape_allowed) { - /* hint allows both landscape and portrait, promote to full user */ - req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER; - } else { - /* Use the only one allowed "orientation" */ - req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); - } - } else { - /* Fixed window and both orientations are allowed. Choose one. */ - if (is_portrait_allowed && is_landscape_allowed) { - req = (w > h ? orientation_landscape : orientation_portrait); - } else { - /* Use the only one allowed "orientation" */ - req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); - } - } - } - - Log.v(TAG, "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); - mSingleton.setRequestedOrientation(req); - } - - /** - * This method is called by SDL using JNI. - */ - public static void minimizeWindow() { - - if (mSingleton == null) { - return; - } - - Intent startMain = new Intent(Intent.ACTION_MAIN); - startMain.addCategory(Intent.CATEGORY_HOME); - startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mSingleton.startActivity(startMain); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean shouldMinimizeOnFocusLoss() { -/* - if (Build.VERSION.SDK_INT >= 24) { - if (mSingleton == null) { - return true; - } - - if (mSingleton.isInMultiWindowMode()) { - return false; - } - - if (mSingleton.isInPictureInPictureMode()) { - return false; - } - } - - return true; -*/ - return false; - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean isScreenKeyboardShown() - { - if (mTextEdit == null) { - return false; - } - - if (!mScreenKeyboardShown) { - return false; - } - - InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - return imm.isAcceptingText(); - - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean supportsRelativeMouse() - { - // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under - // Android 7 APIs, and simply returns no data under Android 8 APIs. - // - // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and - // thus SDK version 27. If we are in DeX mode and not API 27 or higher, as a result, - // we should stick to relative mode. - // - if (Build.VERSION.SDK_INT < 27 /* Android 8.1 (O_MR1) */ && isDeXMode()) { - return false; - } - - return SDLActivity.getMotionListener().supportsRelativeMouse(); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean setRelativeMouseEnabled(boolean enabled) - { - if (enabled && !supportsRelativeMouse()) { - return false; - } - - return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean sendMessage(int command, int param) { - if (mSingleton == null) { - return false; - } - return mSingleton.sendCommand(command, param); - } - - /** - * This method is called by SDL using JNI. - */ - public static Context getContext() { - return SDL.getContext(); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean isAndroidTV() { - UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE); - if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { - return true; - } - if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) { - return true; - } - if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { - return true; - } - return Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV"); - } - - public static double getDiagonal() - { - DisplayMetrics metrics = new DisplayMetrics(); - Activity activity = (Activity)getContext(); - if (activity == null) { - return 0.0; - } - activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); - - double dWidthInches = metrics.widthPixels / (double)metrics.xdpi; - double dHeightInches = metrics.heightPixels / (double)metrics.ydpi; - - return Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches)); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean isTablet() { - // If our diagonal size is seven inches or greater, we consider ourselves a tablet. - return (getDiagonal() >= 7.0); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean isChromebook() { - if (getContext() == null) { - return false; - } - return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean isDeXMode() { - if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { - return false; - } - try { - final Configuration config = getContext().getResources().getConfiguration(); - final Class configClass = config.getClass(); - return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass) - == configClass.getField("semDesktopModeEnabled").getInt(config); - } catch(Exception ignored) { - return false; - } - } - - /** - * This method is called by SDL using JNI. - */ - public static DisplayMetrics getDisplayDPI() { - return getContext().getResources().getDisplayMetrics(); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean getManifestEnvironmentVariables() { - try { - if (getContext() == null) { - return false; - } - - ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); - Bundle bundle = applicationInfo.metaData; - if (bundle == null) { - return false; - } - String prefix = "SDL_ENV."; - final int trimLength = prefix.length(); - for (String key : bundle.keySet()) { - if (key.startsWith(prefix)) { - String name = key.substring(trimLength); - String value = bundle.get(key).toString(); - nativeSetenv(name, value); - } - } - /* environment variables set! */ - return true; - } catch (Exception e) { - Log.v(TAG, "exception " + e.toString()); - } - return false; - } - - // This method is called by SDLControllerManager's API 26 Generic Motion Handler. - public static View getContentView() { - return mLayout; - } - - static class ShowTextInputTask implements Runnable { - /* - * This is used to regulate the pan&scan method to have some offset from - * the bottom edge of the input region and the top edge of an input - * method (soft keyboard) - */ - static final int HEIGHT_PADDING = 15; - - public int x, y, w, h; - - public ShowTextInputTask(int x, int y, int w, int h) { - this.x = x; - this.y = y; - this.w = w; - this.h = h; - - /* Minimum size of 1 pixel, so it takes focus. */ - if (this.w <= 0) { - this.w = 1; - } - if (this.h + HEIGHT_PADDING <= 0) { - this.h = 1 - HEIGHT_PADDING; - } - } - - @Override - public void run() { - RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); - params.leftMargin = x; - params.topMargin = y; - - if (mTextEdit == null) { - mTextEdit = new DummyEdit(SDL.getContext()); - - mLayout.addView(mTextEdit, params); - } else { - mTextEdit.setLayoutParams(params); - } - - mTextEdit.setVisibility(View.VISIBLE); - mTextEdit.requestFocus(); - - InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mTextEdit, 0); - - mScreenKeyboardShown = true; - } - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean showTextInput(int x, int y, int w, int h) { - // Transfer the task to the main thread as a Runnable - return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); - } - - public static boolean isTextInputEvent(KeyEvent event) { - - // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT - if (event.isCtrlPressed()) { - return false; - } - - return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; - } - - public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputConnection ic) { - int deviceId = event.getDeviceId(); - int source = event.getSource(); - - if (source == InputDevice.SOURCE_UNKNOWN) { - InputDevice device = InputDevice.getDevice(deviceId); - if (device != null) { - source = device.getSources(); - } - } - -// if (event.getAction() == KeyEvent.ACTION_DOWN) { -// Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); -// } else if (event.getAction() == KeyEvent.ACTION_UP) { -// Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); -// } - - // Dispatch the different events depending on where they come from - // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD - // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD - // - // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and - // SOURCE_JOYSTICK, while its key events arrive from the keyboard source - // So, retrieve the device itself and check all of its sources - if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { - // Note that we process events with specific key codes here - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) { - return true; - } - } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) { - return true; - } - } - } - - if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { - // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses - // they are ignored here because sending them as mouse input to SDL is messy - if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { - switch (event.getAction()) { - case KeyEvent.ACTION_DOWN: - case KeyEvent.ACTION_UP: - // mark the event as handled or it will be handled by system - // handling KEYCODE_BACK by system will call onBackPressed() - return true; - } - } - } - - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (isTextInputEvent(event)) { - if (ic != null) { - ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); - } else { - SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); - } - } - onNativeKeyDown(keyCode); - return true; - } else if (event.getAction() == KeyEvent.ACTION_UP) { - onNativeKeyUp(keyCode); - return true; - } - - return false; - } - - /** - * This method is called by SDL using JNI. - */ - public static Surface getNativeSurface() { - if (SDLActivity.mSurface == null) { - return null; - } - return SDLActivity.mSurface.getNativeSurface(); - } - - // Input - - /** - * This method is called by SDL using JNI. - */ - public static void initTouch() { - int[] ids = InputDevice.getDeviceIds(); - - for (int id : ids) { - InputDevice device = InputDevice.getDevice(id); - /* Allow SOURCE_TOUCHSCREEN and also Virtual InputDevices because they can send TOUCHSCREEN events */ - if (device != null && ((device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN - || device.isVirtual())) { - - int touchDevId = device.getId(); - /* - * Prevent id to be -1, since it's used in SDL internal for synthetic events - * Appears when using Android emulator, eg: - * adb shell input mouse tap 100 100 - * adb shell input touchscreen tap 100 100 - */ - if (touchDevId < 0) { - touchDevId -= 1; - } - nativeAddTouch(touchDevId, device.getName()); - } - } - } - - // Messagebox - - /** Result of current messagebox. Also used for blocking the calling thread. */ - protected final int[] messageboxSelection = new int[1]; - - /** - * This method is called by SDL using JNI. - * Shows the messagebox from UI thread and block calling thread. - * buttonFlags, buttonIds and buttonTexts must have same length. - * @param buttonFlags array containing flags for every button. - * @param buttonIds array containing id for every button. - * @param buttonTexts array containing text for every button. - * @param colors null for default or array of length 5 containing colors. - * @return button id or -1. - */ - public int messageboxShowMessageBox( - final int flags, - final String title, - final String message, - final int[] buttonFlags, - final int[] buttonIds, - final String[] buttonTexts, - final int[] colors) { - - messageboxSelection[0] = -1; - - // sanity checks - - if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { - return -1; // implementation broken - } - - // collect arguments for Dialog - - final Bundle args = new Bundle(); - args.putInt("flags", flags); - args.putString("title", title); - args.putString("message", message); - args.putIntArray("buttonFlags", buttonFlags); - args.putIntArray("buttonIds", buttonIds); - args.putStringArray("buttonTexts", buttonTexts); - args.putIntArray("colors", colors); - - // trigger Dialog creation on UI thread - - runOnUiThread(new Runnable() { - @Override - public void run() { - messageboxCreateAndShow(args); - } - }); - - // block the calling thread - - synchronized (messageboxSelection) { - try { - messageboxSelection.wait(); - } catch (InterruptedException ex) { - ex.printStackTrace(); - return -1; - } - } - - // return selected value - - return messageboxSelection[0]; - } - - protected void messageboxCreateAndShow(Bundle args) { - - // TODO set values from "flags" to messagebox dialog - - // get colors - - int[] colors = args.getIntArray("colors"); - int backgroundColor; - int textColor; - int buttonBorderColor; - int buttonBackgroundColor; - int buttonSelectedColor; - if (colors != null) { - int i = -1; - backgroundColor = colors[++i]; - textColor = colors[++i]; - buttonBorderColor = colors[++i]; - buttonBackgroundColor = colors[++i]; - buttonSelectedColor = colors[++i]; - } else { - backgroundColor = Color.TRANSPARENT; - textColor = Color.TRANSPARENT; - buttonBorderColor = Color.TRANSPARENT; - buttonBackgroundColor = Color.TRANSPARENT; - buttonSelectedColor = Color.TRANSPARENT; - } - - // create dialog with title and a listener to wake up calling thread - - final AlertDialog dialog = new AlertDialog.Builder(this).create(); - dialog.setTitle(args.getString("title")); - dialog.setCancelable(false); - dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface unused) { - synchronized (messageboxSelection) { - messageboxSelection.notify(); - } - } - }); - - // create text - - TextView message = new TextView(this); - message.setGravity(Gravity.CENTER); - message.setText(args.getString("message")); - if (textColor != Color.TRANSPARENT) { - message.setTextColor(textColor); - } - - // create buttons - - int[] buttonFlags = args.getIntArray("buttonFlags"); - int[] buttonIds = args.getIntArray("buttonIds"); - String[] buttonTexts = args.getStringArray("buttonTexts"); - - final SparseArray