diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7abef73e5..41758382cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,7 +76,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: dusklight-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}} + name: dusklight-${{env.APP_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}} path: | build/install/Dusklight-*.AppImage build/install/debug.tar.* @@ -145,7 +145,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: dusklight-${{env.DUSK_VERSION}}-${{matrix.artifact_name}} + name: dusklight-${{env.APP_VERSION}}-${{matrix.artifact_name}} path: | build/install/Dusklight.app build/install/debug.tar.* @@ -221,7 +221,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: dusklight-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}} + name: dusklight-${{env.APP_VERSION}}-android-${{matrix.artifact_arch}} path: upload/ build-windows: @@ -283,7 +283,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: dusklight-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}} + name: dusklight-${{env.APP_VERSION}}-win32-msvc-${{matrix.artifact_arch}} path: | build/install/*.exe build/install/*.dll diff --git a/.gitmodules b/.gitmodules index b386c1754a..f05842fa7b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "extern/aurora"] path = extern/aurora url = https://github.com/encounter/aurora.git +[submodule "extern/borealis"] + path = extern/borealis + url = https://github.com/encounter/borealis.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d5d63d621..c534b14d9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,10 +5,11 @@ if (NOT CMAKE_BUILD_TYPE) "Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE) endif () -include(cmake/DetectVersion.cmake) -detect_version() +include(extern/borealis/cmake/DetectVersion.cmake) +borealis_detect_version() message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") -project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING}) +project(dusklight LANGUAGES C CXX VERSION ${BOREALIS_APP_VERSION}) + if (APPLE) enable_language(OBJC OBJCXX) endif () @@ -74,6 +75,8 @@ set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE) add_subdirectory(extern/aurora EXCLUDE_FROM_ALL) target_compile_definitions(aurora_mtx PRIVATE MTX_USE_PS=1) +add_subdirectory(extern/borealis EXCLUDE_FROM_ALL) + add_subdirectory(libs/freeverb) if (CMAKE_BUILD_TYPE STREQUAL "Debug") @@ -85,8 +88,6 @@ endif () option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)") option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.") option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON) -option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON) -option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF) option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF) option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT}) option(DUSK_ENABLE_CODE_MODS "Enable code mods" OFF) @@ -235,18 +236,6 @@ endif () include(FetchContent) # Declare all dependencies first so CMake can download them in parallel -message(STATUS "dusklight: Fetching cxxopts") -FetchContent_Declare(cxxopts - URL https://github.com/jarro2783/cxxopts/archive/refs/tags/v3.3.1.tar.gz - URL_HASH SHA256=3bfc70542c521d4b55a46429d808178916a579b28d048bd8c727ee76c39e2072 - DOWNLOAD_EXTRACT_TIMESTAMP FALSE -) -message(STATUS "dusklight: Fetching nlohmann/json") -FetchContent_Declare(json - URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz - URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa - DOWNLOAD_EXTRACT_TIMESTAMP FALSE -) message(STATUS "dusklight: Fetching miniz") FetchContent_Declare(miniz URL https://github.com/richgel999/miniz/releases/download/3.0.2/miniz-3.0.2.zip @@ -254,7 +243,7 @@ FetchContent_Declare(miniz EXCLUDE_FROM_ALL ) -set(_fetch_content_deps cxxopts json miniz) +set(_fetch_content_deps miniz) if (DUSK_HAS_FUNCHOOK) message(STATUS "dusklight: Fetching funchook") # cmake/PatchFunchook.cmake patches funchook's cmake/capstone.cmake.in to inject a @@ -289,40 +278,12 @@ if (DUSK_HAS_FUNCHOOK) endif () FetchContent_MakeAvailable(${_fetch_content_deps}) -if (DUSK_ENABLE_SENTRY_NATIVE) - message(STATUS "dusklight: Fetching sentry-native") - set(SENTRY_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) - set(SENTRY_BACKEND crashpad CACHE STRING "" FORCE) - if (WIN32) - set(SENTRY_TRANSPORT winhttp CACHE STRING "" FORCE) - endif () - set(SENTRY_BUILD_TESTS OFF CACHE BOOL "" FORCE) - set(SENTRY_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) - set(SENTRY_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) - FetchContent_Declare(sentry_native - GIT_REPOSITORY https://github.com/getsentry/sentry-native.git - GIT_TAG 0.13.6 - GIT_SHALLOW TRUE - GIT_PROGRESS TRUE - GIT_SUBMODULES_RECURSE TRUE - ) - if (NOT sentry_native_POPULATED) - FetchContent_Populate(sentry_native) - set(_skip_install_rules ${CMAKE_SKIP_INSTALL_RULES}) - set(CMAKE_SKIP_INSTALL_RULES ON) - add_subdirectory(${sentry_native_SOURCE_DIR} ${sentry_native_BINARY_DIR} EXCLUDE_FROM_ALL) - set(CMAKE_SKIP_INSTALL_RULES ${_skip_install_rules}) - endif () -endif () - # Use signed char on ARM to match the original game (and x86) string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _arch) if(_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU") add_compile_options(-fsigned-char) endif() -configure_version_header() - include(files.cmake) # TODO: version handling for res includes @@ -335,67 +296,23 @@ set(DUSK_PRODUCT_NAME "Dusklight") set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors") source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES}) -source_group("dusklight" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES}) +source_group("dusklight" FILES ${DUSK_FILES}) include(cmake/GameABIConfig.cmake) find_package(Threads REQUIRED) set(GAME_COMPILE_DEFS DUSK_BUILDING_GAME=1) set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd - aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt + aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::log borealis::presentation borealis::sentry borealis::update freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt Threads::Threads zstd::libzstd dusklight_game_headers) if (DUSK_HAS_FUNCHOOK) list(APPEND GAME_LIBS funchook-static) endif () -if (DUSK_ENABLE_SENTRY_NATIVE) - list(APPEND GAME_LIBS sentry) - list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_SENTRY_NATIVE=1 SENTRY_BUILD_STATIC=1) -endif () - if (WIN32) list(APPEND GAME_LIBS Ws2_32) - if (CMAKE_BUILD_TYPE STREQUAL Debug) - list(APPEND GAME_LIBS dbghelp) - list(APPEND GAME_COMPILE_DEFS DUSK_CRASH_DBGHELP=1) - endif () endif () -set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/no_backend.cpp) -if (DUSK_ENABLE_UPDATE_CHECKER) - list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_UPDATE_CHECKER=1) - if (WIN32) - set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/winhttp.cpp) - list(APPEND GAME_LIBS winhttp) - list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_WINHTTP=1) - message(STATUS "dusklight: Enabled update checker (WinHTTP)") - elseif (ANDROID) - set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/android.cpp) - list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_ANDROID=1) - message(STATUS "dusklight: Enabled update checker (Android)") - elseif (APPLE) - find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED) - set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/url_session.mm) - set_source_files_properties(src/dusk/http/url_session.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) - list(APPEND GAME_LIBS ${FOUNDATION_FRAMEWORK}) - list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_URLSESSION=1) - message(STATUS "dusklight: Enabled update checker (NSURLSession)") - elseif (CMAKE_SYSTEM_NAME STREQUAL Linux) - find_package(CURL QUIET OPTIONAL_COMPONENTS HTTPS SSL) - if (CURL_FOUND AND CURL_HTTPS_FOUND AND CURL_SSL_FOUND) - set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/curl.cpp) - list(APPEND GAME_LIBS CURL::libcurl) - list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_LIBCURL=1) - message(STATUS "dusklight: Enabled update checker (libcurl)") - else () - message(STATUS "dusklight: Disabled update checker (libcurl + HTTPS/SSL not found)") - endif () - else () - message(STATUS "dusklight: Disabled update checker (unsupported platform)") - endif () -endif () -list(APPEND DUSK_FILES ${DUSK_HTTP_BACKEND_SOURCE}) - if (DUSK_MOVIE_SUPPORT) if (TARGET libjpeg-turbo::turbojpeg-static) list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static) @@ -405,15 +322,6 @@ if (DUSK_MOVIE_SUPPORT) list(APPEND GAME_COMPILE_DEFS MOVIE_SUPPORT=1) endif () -set(DUSK_ENABLE_DISCORD_DEFAULT ON) -if (DEFINED DUSK_ENABLE_DISCORD_RPC AND NOT DEFINED DUSK_ENABLE_DISCORD) - set(DUSK_ENABLE_DISCORD_DEFAULT ${DUSK_ENABLE_DISCORD_RPC}) -endif () -option(DUSK_ENABLE_DISCORD "Enable Discord Rich Presence support" ${DUSK_ENABLE_DISCORD_DEFAULT}) -if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS) - list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1) -endif () - if (DUSK_ENABLE_CODE_MODS) list(APPEND GAME_COMPILE_DEFS DUSK_CODE_MODS=1) endif () @@ -478,10 +386,10 @@ endif () set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES} ${miniz_SOURCE_DIR}/miniz.c) if(ANDROID) add_library(dusklight SHARED ${DUSK_FILES}) - set_target_properties(dusklight PROPERTIES OUTPUT_NAME main) else () add_executable(dusklight ${DUSK_FILES}) endif () +borealis_configure_android_application(dusklight) if (ENABLE_ASAN) target_sources(dusklight PRIVATE src/dusk/asan_options.c) endif () @@ -559,17 +467,9 @@ if (TARGET crashpad_handler) ) endif () -if (ANDROID) - # SDLActivity loads SDL_main via dlsym on Android. Since aurora::main is a static - # archive, force an undefined reference so the linker keeps the SDL_main object. - target_link_options(dusklight PRIVATE "-Wl,-u,SDL_main") -endif () - if (CMAKE_SYSTEM_NAME STREQUAL Linux) target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1") target_link_libraries(dusklight PRIVATE dl) -elseif (ANDROID) - target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1") endif () if (NOT APPLE) @@ -652,8 +552,8 @@ if (APPLE) MACOSX_BUNDLE TRUE MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME} MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER} - MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING} - MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING} + MACOSX_BUNDLE_BUNDLE_VERSION ${BOREALIS_APP_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${BOREALIS_APP_SHORT_VERSION} MACOSX_BUNDLE_INFO_PLIST ${DUSK_INFO_PLIST} OUTPUT_NAME Dusklight XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES" @@ -686,21 +586,6 @@ if (APPLE) endif () endif () -if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") - find_library(APPKIT_FRAMEWORK AppKit REQUIRED) - target_sources(dusklight PRIVATE src/dusk/file_select_macos.mm) - set_source_files_properties(src/dusk/file_select_macos.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) - target_link_libraries(dusklight PRIVATE ${APPKIT_FRAMEWORK}) -endif () - -if (IOS) - find_library(UIKIT_FRAMEWORK UIKit REQUIRED) - find_library(UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK UniformTypeIdentifiers REQUIRED) - target_sources(dusklight PRIVATE src/dusk/ios/FileSelectDialog.m) - set_source_files_properties(src/dusk/ios/FileSelectDialog.m PROPERTIES COMPILE_FLAGS -fobjc-arc) - target_link_libraries(dusklight PRIVATE ${UIKIT_FRAMEWORK} ${UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK}) -endif () - include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake) aurora_copy_runtime_dlls(dusklight) diff --git a/CMakePresets.json b/CMakePresets.json index 4faccc4448..45a6522625 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -28,12 +28,12 @@ "cacheVariables": { "CMAKE_C_COMPILER_LAUNCHER": "sccache", "CMAKE_CXX_COMPILER_LAUNCHER": "sccache", - "DUSK_ENABLE_SENTRY_NATIVE": { + "BOREALIS_ENABLE_SENTRY": { "type": "BOOL", "value": true }, - "DUSK_SENTRY_DSN": "$env{SENTRY_DSN}", - "DUSK_SENTRY_ENVIRONMENT": "production", + "BOREALIS_SENTRY_DSN": "$env{SENTRY_DSN}", + "BOREALIS_SENTRY_ENVIRONMENT": "production", "Rust_RUSTUP_INSTALL_MISSING_TARGET": { "type": "BOOL", "value": true @@ -448,7 +448,7 @@ "ci" ], "cacheVariables": { - "DUSK_ENABLE_SENTRY_NATIVE": { + "BOREALIS_ENABLE_SENTRY": { "type": "BOOL", "value": false } diff --git a/ci/build-appimage.sh b/ci/build-appimage.sh index e12f1163b9..c4a0e82995 100755 --- a/ci/build-appimage.sh +++ b/ci/build-appimage.sh @@ -23,5 +23,5 @@ cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,102 cp platforms/freedesktop/dev.twilitrealm.dusk.desktop build/appdir/usr/share/applications cd build/install -VERSION="$DUSK_VERSION" NO_STRIP=1 "$linuxdeploy" \ +VERSION="$APP_VERSION" NO_STRIP=1 "$linuxdeploy" \ -l "$lib_dir/libusb-1.0.so" --appdir "$build_dir/appdir" --output appimage diff --git a/cmake/DetectVersion.cmake b/cmake/DetectVersion.cmake deleted file mode 100644 index ec72a97a2e..0000000000 --- a/cmake/DetectVersion.cmake +++ /dev/null @@ -1,121 +0,0 @@ -# Version detection shared by the main build and the mod SDK (sdk/CMakeLists.txt) -include_guard(GLOBAL) - -get_filename_component(_DUSK_VERSION_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) - -set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN") -set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment") - -set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)") - -macro(detect_version) - if (DUSK_VERSION_OVERRIDE) - set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}") - set(DUSK_VERSION_STRING "0.0.0.0") - set(DUSK_SHORT_VERSION_STRING "0.0.0") - set(DUSK_VERSION_CODE "1") - set(DUSK_WC_REVISION "") - set(DUSK_WC_BRANCH "") - set(DUSK_WC_DATE "") - message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}") - else () - # obtain revision info from git - find_package(Git) - if (GIT_FOUND) - # make sure version information gets re-run when the current Git HEAD changes - execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD - OUTPUT_VARIABLE dusk_git_head_filename - OUTPUT_STRIP_TRAILING_WHITESPACE) - get_filename_component(dusk_git_head_filename "${dusk_git_head_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}") - - execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD - OUTPUT_VARIABLE dusk_git_head_symbolic - OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} - COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic} - OUTPUT_VARIABLE dusk_git_head_symbolic_filename - OUTPUT_STRIP_TRAILING_WHITESPACE) - get_filename_component(dusk_git_head_symbolic_filename "${dusk_git_head_symbolic_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}") - - # defines DUSK_WC_REVISION - execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD - OUTPUT_VARIABLE DUSK_WC_REVISION - OUTPUT_STRIP_TRAILING_WHITESPACE) - # defines DUSK_WC_DESCRIBE - execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*" - OUTPUT_VARIABLE DUSK_WC_DESCRIBE - OUTPUT_STRIP_TRAILING_WHITESPACE) - - # remove the git hash, then collapse a clean "-0" suffix only - string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}") - string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}") - - # defines DUSK_WC_BRANCH - execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD - OUTPUT_VARIABLE DUSK_WC_BRANCH - OUTPUT_STRIP_TRAILING_WHITESPACE) - # defines DUSK_WC_DATE - execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad - OUTPUT_VARIABLE DUSK_WC_DATE - OUTPUT_STRIP_TRAILING_WHITESPACE) - else () - message(STATUS "Unable to find git, commit information will not be available") - endif () - - if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$") - set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") - set(_ver_major ${CMAKE_MATCH_1}) - set(_ver_minor ${CMAKE_MATCH_2}) - set(_ver_patch ${CMAKE_MATCH_3}) - set(DUSK_VERSION_TWEAK "0") - if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$") - set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}") - elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$") - set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}") - endif () - set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}") - if (DUSK_VERSION_TWEAK GREATER 999) - set(_tweak 999) - else () - set(_tweak ${DUSK_VERSION_TWEAK}) - endif () - # encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999 - math(EXPR DUSK_VERSION_CODE - "${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}") - else () - set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION") - set(DUSK_VERSION_STRING "0.0.0.0") - set(DUSK_SHORT_VERSION_STRING "0.0.0") - set(DUSK_VERSION_CODE "1") - endif () - - endif () - - # Add version information to CI environment variables - if (DEFINED ENV{GITHUB_ENV}) - file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n") - file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n") - endif () - message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}") -endmacro() - -# Sets PLATFORM_NAME and configures version.h into the caller's binary dir. -macro(configure_version_header) - if (CMAKE_SYSTEM_NAME STREQUAL Windows) - set(PLATFORM_NAME win32) - elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin) - if (IOS) - set(PLATFORM_NAME ios) - elseif (TVOS) - set(PLATFORM_NAME tvos) - else () - set(PLATFORM_NAME macos) - endif () - else () - string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME) - endif () - - configure_file(${_DUSK_VERSION_ROOT}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) -endmacro() diff --git a/extern/aurora b/extern/aurora index 6c4c27f9e8..5027ed63a7 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 6c4c27f9e8e40f584d27726655d80ec85a5a7d2c +Subproject commit 5027ed63a73dfba28de9eceed00481fb09a19c35 diff --git a/extern/borealis b/extern/borealis new file mode 160000 index 0000000000..eec74b6dec --- /dev/null +++ b/extern/borealis @@ -0,0 +1 @@ +Subproject commit eec74b6decf364354d0a94ad308ea6d0186585a2 diff --git a/files.cmake b/files.cmake index 9867b2d39a..4fddf9b9dd 100644 --- a/files.cmake +++ b/files.cmake @@ -1421,30 +1421,21 @@ set(DUSK_FILES src/dusk/achievements.cpp src/dusk/action_bindings.cpp src/dusk/action_bindings.h - src/dusk/android_frame_rate.cpp - src/dusk/android_frame_rate.hpp src/dusk/asserts.cpp src/dusk/autosave.cpp src/dusk/config.cpp src/dusk/config.hpp - src/dusk/crash_handler.cpp - src/dusk/crash_reporting.cpp src/dusk/data.cpp src/dusk/data.hpp - src/dusk/discord.cpp - src/dusk/discord.hpp src/dusk/discord_presence.cpp src/dusk/dvd_asset.cpp src/dusk/dvd_asset.hpp src/dusk/extras.c - src/dusk/file_select.cpp - src/dusk/file_select.hpp src/dusk/frame_interpolation.cpp src/dusk/game_clock.cpp src/dusk/gamepad_color.cpp src/dusk/globals.cpp src/dusk/gyro.cpp - src/dusk/http/http.hpp src/dusk/imgui/ImGuiActorSpawner.cpp src/dusk/imgui/ImGuiBloomWindow.cpp src/dusk/imgui/ImGuiBloomWindow.hpp @@ -1507,6 +1498,8 @@ set(DUSK_FILES src/dusk/mods/svc/stage.cpp src/dusk/mods/svc/stage.hpp src/dusk/mouse.cpp + src/dusk/presentation.cpp + src/dusk/presentation.hpp src/dusk/scope_guard.hpp src/dusk/settings.cpp src/dusk/speedrun.cpp @@ -1582,8 +1575,6 @@ set(DUSK_FILES src/dusk/ui/warp.hpp src/dusk/ui/window.cpp src/dusk/ui/window.hpp - src/dusk/update_check.cpp - src/dusk/update_check.hpp src/dusk/version.cpp src/dusk/utilities.cpp src/helpers/batch.cpp @@ -1591,10 +1582,3 @@ set(DUSK_FILES src/helpers/offset_ptr.cpp src/helpers/string.cpp ) - -set(DUSK_HTTP_BACKEND_FILES - src/dusk/http/no_backend.cpp - src/dusk/http/curl.cpp - src/dusk/http/winhttp.cpp - src/dusk/http/url_session.mm -) diff --git a/flake.nix b/flake.nix index c470446113..01a20fad2d 100644 --- a/flake.nix +++ b/flake.nix @@ -228,7 +228,7 @@ ninjaFlags = [ "dusklight" ]; cmakeFlags = [ - "-DDUSK_VERSION_OVERRIDE=${versionSuffix}" + "-DBOREALIS_APP_VERSION_OVERRIDE=${versionSuffix}" "-DFETCHCONTENT_FULLY_DISCONNECTED=ON" "-DAURORA_DAWN_PROVIDER=package" "-DAURORA_DAWN_LINKAGE=static" diff --git a/platforms/android/README.md b/platforms/android/README.md index 7a5d01ee44..6ebcac23cd 100644 --- a/platforms/android/README.md +++ b/platforms/android/README.md @@ -1,6 +1,6 @@ # Android Shell -This directory contains a minimal SDLActivity-based Android app wrapper for Dusklight. +This directory contains Dusklight's Android shell built on top of Borealis. ## Prerequisites @@ -25,24 +25,19 @@ cmake --build --preset android-arm64 This build produces `build/android-arm64/libmain.so` -## Refresh SDL Java Shim (Optional) - -If you update SDL and want to refresh the embedded Java shim files: - -```bash -./android/scripts/sync-sdl-java.sh -``` - ## Build APK ```bash -cd android +cd platforms/android ./gradlew :app:assembleDebug ``` Output APK: -- `android/app/build/outputs/apk/debug/app-debug.apk` +- `app/build/outputs/apk/debug/app-arm64-v8a-debug.apk` + +Aurora needs a hardware-backed graphics adapter. If an AVD has GPU +acceleration disabled, launch it with `-gpu host`. ## Launch With Runtime Args (adb) @@ -50,10 +45,13 @@ You can pass command-line args through the activity intent: ```bash adb shell am start -n dev.twilitrealm.dusk/.DuskActivity \ - --es dusk_args "--backend vulkan" + --es borealis_args "--backend vulkan" ``` Supported extras: -- `dusk_args`: single shell-like argument string -- `dusk_argv`: string-array argv +- `borealis_args`: single shell-like argument string +- `borealis_argv`: string-array argv + +The legacy `dusk_args` and `dusk_argv` names remain accepted during the shell +transition. diff --git a/platforms/android/app/build.gradle b/platforms/android/app/build.gradle index 08d04bf35f..aba966b448 100644 --- a/platforms/android/app/build.gradle +++ b/platforms/android/app/build.gradle @@ -2,163 +2,32 @@ plugins { id 'com.android.application' } -def versionNameStr = (System.getenv("DUSK_VERSION") ?: "v0.1.0").replaceFirst("^v", "") -def versionCodeInt = (System.getenv("DUSK_VERSION_CODE") ?: "100000").toInteger() - def duskRepoDir = rootProject.projectDir.parentFile.parentFile -def androidNativeBuildDir = new File(duskRepoDir, 'build/android-arm64') -def nativeLibrary = new File(androidNativeBuildDir, 'libmain.so') -def stageStripValue = providers.gradleProperty('ANDROID_STAGE_STRIP') - .orElse(providers.gradleProperty('androidStageStrip')) - .orElse(providers.environmentVariable('ANDROID_STAGE_STRIP')) - .orElse('1') -def androidHome = { - def sdkPath = System.getenv('ANDROID_HOME') - if (!sdkPath) { - throw new GradleException('ANDROID_HOME is not available') - } - def sdkDir = new File(sdkPath) - if (!sdkDir.isDirectory()) { - throw new GradleException("ANDROID_HOME points to a missing directory: ${sdkDir}") - } - sdkDir -}.memoize() +def borealisDir = new File(duskRepoDir, 'extern/borealis') -def androidNdkVersion = { - def ndkVersion = System.getenv('ANDROID_NDK_VERSION') - if (!ndkVersion) { - throw new GradleException('ANDROID_NDK_VERSION is not available') - } - ndkVersion -}.memoize() +ext.borealisAndroid = [ + borealisDir: borealisDir, + propertiesFile: new File(duskRepoDir, 'build/android-arm64/borealis-android.properties'), + namespace: 'dev.twilitrealm.dusk', + applicationId: 'dev.twilitrealm.dusk', + abis: ['arm64-v8a'], + assets: [ + [ + from: new File(duskRepoDir, 'res'), + into: 'res', + excludes: ['**/.DS_Store'] + ], + [ + from: new File(duskRepoDir, 'build/android-arm64/bundled_mods'), + into: 'mods', + includes: ['*.dusk'] + ] + ], + proguardRules: file('proguard-rules.pro') +] -def androidNdkDir = { - def ndkDir = new File(androidHome(), "ndk/${androidNdkVersion()}") - if (!new File(ndkDir, 'build/cmake/android.toolchain.cmake').isFile()) { - throw new GradleException( - "Android NDK ${androidNdkVersion()} is missing or invalid at ${ndkDir}" - ) - } - ndkDir -}.memoize() - -def llvmPrebuiltDir = { - def prebuiltRoot = new File(androidNdkDir(), 'toolchains/llvm/prebuilt') - def prebuiltDir = (prebuiltRoot.listFiles()?.findAll { it.isDirectory() } ?: []) - .sort { it.name } - .find { candidate -> - new File( - candidate, - 'sysroot/usr/lib/aarch64-linux-android/libc++_shared.so' - ).isFile() - } - if (!prebuiltDir) { - throw new GradleException("Cannot find NDK libc++_shared.so under ${prebuiltRoot}") - } - prebuiltDir -}.memoize() - -def stlLibrary = { - def library = new File( - llvmPrebuiltDir(), - 'sysroot/usr/lib/aarch64-linux-android/libc++_shared.so' - ) - if (!library.isFile()) { - throw new GradleException("libc++_shared.so is missing") - } - library -}.memoize() - -def stagedJniLibsDir = layout.buildDirectory.dir('generated/jniLibs/dusklight') - -def stageJniLibs = tasks.register('stageJniLibs', Sync) { - group = 'build' - from(nativeLibrary) { - rename { 'libmain.so' } - into 'arm64-v8a' - } - from(providers.provider { stlLibrary() }) { - rename { 'libc++_shared.so' } - into 'arm64-v8a' - } - into(stagedJniLibsDir) - - doFirst { - if (!nativeLibrary.isFile()) { - throw new GradleException("Native library is missing") - } - } -} - -def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusklight') -def syncDuskAssets = tasks.register('syncDuskAssets', Sync) { - from(new File(duskRepoDir, 'res')) { - into 'res' - exclude '**/.DS_Store' - } - from(new File(androidNativeBuildDir, 'bundled_mods')) { - into 'mods' - include '*.dusk' - } - into(duskGeneratedAssetsDir) -} - -android { - namespace 'dev.twilitrealm.dusk' - compileSdk 36 - ndkVersion androidNdkVersion() - - defaultConfig { - applicationId 'dev.twilitrealm.dusk' - minSdk 26 - targetSdk 36 - versionCode versionCodeInt - versionName versionNameStr - } - - buildTypes { - debug { - minifyEnabled false - } - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - sourceSets { - main { - jniLibs.srcDirs = [stagedJniLibsDir] - assets.srcDirs = [duskGeneratedAssetsDir] - } - } - - packaging { - jniLibs { - if (stageStripValue.get() == '0') { - keepDebugSymbols += '**/libmain.so' - } - } - } - - splits { - abi { - enable true - reset() - include 'arm64-v8a' - universalApk false - } - } - - lint { - abortOnError false - } -} +apply from: new File(borealisDir, 'platforms/android/gradle/borealis-application.gradle') dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) } - -tasks.named('preBuild').configure { - dependsOn(stageJniLibs, syncDuskAssets) -} diff --git a/platforms/android/app/proguard-rules.pro b/platforms/android/app/proguard-rules.pro index 72b7ca16fa..308a46ab83 100644 --- a/platforms/android/app/proguard-rules.pro +++ b/platforms/android/app/proguard-rules.pro @@ -1,4 +1 @@ -# Keep SDL activity and related JNI bridge methods. --keep class org.libsdl.app.** { *; } --keep class dev.twilitrealm.dusk.DuskHttpClient { *; } --keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; } +-keep class dev.twilitrealm.dusk.DuskActivity { *; } diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml index cd687963e1..b7270ee433 100644 --- a/platforms/android/app/src/main/AndroidManifest.xml +++ b/platforms/android/app/src/main/AndroidManifest.xml @@ -26,8 +26,8 @@ android:resource="@xml/game_mode_config" /> diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java index 2fa6cbacb3..4c1d8d41eb 100644 --- a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java +++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java @@ -1,102 +1,23 @@ package dev.twilitrealm.dusk; -import android.app.ActionBar; -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.Context; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; import android.os.Bundle; -import android.os.Environment; -import android.provider.DocumentsContract; -import android.provider.OpenableColumns; -import android.provider.Settings; import android.util.Log; -import android.view.Display; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.View; -import android.view.Window; -import android.view.WindowInsets; -import android.view.WindowInsetsController; -import org.libsdl.app.SDLActivity; -import org.libsdl.app.SDLSurface; +import dev.encounter.borealis.BorealisActivity; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -public class DuskActivity extends SDLActivity { +public class DuskActivity extends BorealisActivity { private static final String TAG = "DuskActivity"; - private static final float DEFAULT_SURFACE_FRAME_RATE = 60.0f; - private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455; - private static final int MANAGE_STORAGE_REQUEST_CODE = 0x4456; - private static final String EXTERNAL_STORAGE_AUTHORITY = - "com.android.externalstorage.documents"; - - private long folderDialogUserdata = 0; - private boolean awaitingManageStoragePermission = false; - - private static native void nativeFolderDialogResult(long userdata, String path, String error); - - private static String[] splitArgs(String raw) { - List out = new ArrayList<>(); - StringBuilder current = new StringBuilder(); - boolean inSingle = false; - boolean inDouble = false; - boolean escaped = false; - - for (int i = 0; i < raw.length(); ++i) { - char c = raw.charAt(i); - if (escaped) { - current.append(c); - escaped = false; - continue; - } - if (c == '\\' && !inSingle) { - escaped = true; - continue; - } - if (c == '"' && !inSingle) { - inDouble = !inDouble; - continue; - } - if (c == '\'' && !inDouble) { - inSingle = !inSingle; - continue; - } - if (!inSingle && !inDouble && Character.isWhitespace(c)) { - if (current.length() > 0) { - out.add(current.toString()); - current.setLength(0); - } - continue; - } - current.append(c); - } - - if (escaped) { - current.append('\\'); - } - if (current.length() > 0) { - out.add(current.toString()); - } - return out.toArray(new String[0]); - } - @Override protected void onCreate(Bundle savedInstanceState) { extractBundledMods(); super.onCreate(savedInstanceState); - hideSystemBars(); } // Bundled mod packages ship as APK assets, which the native loader cannot read directly; @@ -143,459 +64,23 @@ public class DuskActivity extends SDLActivity { file.delete(); } - @Override - protected SDLSurface createSDLSurface(Context context) { - return new DuskSurface(context); - } - - @Override - protected void onResume() { - super.onResume(); - hideSystemBars(); - if (awaitingManageStoragePermission) { - resumeFolderDialogAfterPermissionGrant(); - } - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - if (hasFocus) { - hideSystemBars(); - } - } - - private void hideSystemBars() { - Window window = getWindow(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - window.setDecorFitsSystemWindows(false); - WindowInsetsController ctrl = window.getDecorView().getWindowInsetsController(); - if (ctrl != null) { - ctrl.setSystemBarsBehavior( - WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - ctrl.hide(WindowInsets.Type.systemBars()); - } - } else { - View decorView = window.getDecorView(); - int uiOptions = 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; - decorView.setSystemUiVisibility(uiOptions); - ActionBar actionBar = getActionBar(); - if (actionBar != null) { - actionBar.hide(); - } - } - } - - @Override - protected String[] getLibraries() { - // SDL3 is statically linked into libmain.so in this build. - return new String[] { - "main" - }; - } - - public void setPreferredSurfaceFrameRate(float frameRate) { - runOnUiThread(() -> { - if (mSurface instanceof DuskSurface) { - ((DuskSurface)mSurface).setPreferredFrameRate(frameRate); - } - }); - } - - private static final class DuskSurface extends SDLSurface { - private float preferredFrameRate = DEFAULT_SURFACE_FRAME_RATE; - - DuskSurface(Context context) { - super(context); - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - super.surfaceChanged(holder, format, width, height); - setTargetFrameRate(holder); - } - - void setPreferredFrameRate(float frameRate) { - preferredFrameRate = frameRate; - setTargetFrameRate(getHolder()); - } - - private void setTargetFrameRate(SurfaceHolder holder) { - if (!mIsSurfaceReady || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - return; - } - - Surface surface = holder != null ? holder.getSurface() : getHolder().getSurface(); - if (surface == null || !surface.isValid()) { - return; - } - - float targetFrameRate = getMaxSupportedFrameRate(); - if (preferredFrameRate > 0.0f) { - targetFrameRate = preferredFrameRate; - } - if (targetFrameRate <= 0.0f) { - return; - } - - try { - surface.setFrameRate( - targetFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT); - Log.v(TAG, "Requested surface frame rate " + targetFrameRate + " fps"); - } catch (RuntimeException e) { - Log.w(TAG, "Failed to request surface frame rate", e); - } - } - - private float getMaxSupportedFrameRate() { - if (mDisplay == null) { - return 0.0f; - } - - float maxFrameRate = mDisplay.getRefreshRate(); - Display.Mode[] modes = mDisplay.getSupportedModes(); - if (modes == null) { - return maxFrameRate; - } - - for (Display.Mode mode : modes) { - maxFrameRate = Math.max(maxFrameRate, mode.getRefreshRate()); - } - return maxFrameRate; - } - } - @Override protected String[] getArguments() { + String[] arguments = super.getArguments(); + if (arguments.length > 0) { + return arguments; + } + Intent intent = getIntent(); - if (intent != null) { - String[] argv = intent.getStringArrayExtra("dusk_argv"); - if (argv != null && argv.length > 0) { - return argv; - } - - String rawArgs = intent.getStringExtra("dusk_args"); - if (rawArgs != null) { - String trimmed = rawArgs.trim(); - if (!trimmed.isEmpty()) { - return splitArgs(trimmed); - } - } + if (intent == null) { + return arguments; } - return new String[0]; + String[] argv = intent.getStringArrayExtra("dusk_argv"); + if (argv != null && argv.length > 0) { + return argv; + } + String rawArgs = intent.getStringExtra("dusk_args"); + return rawArgs == null ? arguments : splitArguments(rawArgs.trim()); } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == RESULT_OK) { - persistUriPermissions(data); - } - if (requestCode == FOLDER_DIALOG_REQUEST_CODE) { - finishFolderDialog(resultCode, data); - return; - } - super.onActivityResult(requestCode, resultCode, data); - } - - public boolean showFolderDialog(long userdata) { - if (userdata == 0 || folderDialogUserdata != 0) { - return false; - } - - folderDialogUserdata = userdata; - if (requiresManageStoragePermission() && !hasManageStoragePermission()) { - if (!requestManageStoragePermission()) { - finishFolderDialogWithError("Unable to request Android file access permission"); - return false; - } - return true; - } - - openFolderDialog(); - return true; - } - - private void openFolderDialog() { - runOnUiThread(() -> { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | - Intent.FLAG_GRANT_WRITE_URI_PERMISSION | - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | - Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); - - try { - startActivityForResult(intent, FOLDER_DIALOG_REQUEST_CODE); - } catch (ActivityNotFoundException e) { - Log.w(TAG, "Unable to open folder dialog.", e); - finishFolderDialog(Activity.RESULT_CANCELED, null); - } - }); - } - - private boolean requiresManageStoragePermission() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; - } - - private boolean hasManageStoragePermission() { - return !requiresManageStoragePermission() || Environment.isExternalStorageManager(); - } - - private boolean requestManageStoragePermission() { - if (!requiresManageStoragePermission()) { - return true; - } - - awaitingManageStoragePermission = true; - runOnUiThread(() -> { - if (tryStartManageStorageIntent( - new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - .setData(Uri.parse("package:" + getPackageName()))) || - tryStartManageStorageIntent( - new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION))) - { - return; - } - - finishFolderDialogWithError("Unable to request Android file access permission"); - }); - return true; - } - - private boolean tryStartManageStorageIntent(Intent intent) { - try { - startActivityForResult(intent, MANAGE_STORAGE_REQUEST_CODE); - return true; - } catch (ActivityNotFoundException e) { - Log.w(TAG, "Unable to open all-files access settings.", e); - return false; - } - } - - private void resumeFolderDialogAfterPermissionGrant() { - awaitingManageStoragePermission = false; - if (folderDialogUserdata == 0) { - return; - } - - if (hasManageStoragePermission()) { - openFolderDialog(); - return; - } - - finishFolderDialogWithError( - "Allow \"All files access\" for Dusklight before choosing a custom data folder"); - } - - private void finishFolderDialogWithError(String error) { - long userdata = folderDialogUserdata; - folderDialogUserdata = 0; - awaitingManageStoragePermission = false; - if (userdata != 0) { - nativeFolderDialogResult(userdata, null, error); - } - } - - private void finishFolderDialog(int resultCode, Intent data) { - long userdata = folderDialogUserdata; - folderDialogUserdata = 0; - if (userdata == 0) { - return; - } - - if (resultCode == RESULT_OK && data != null && data.getData() != null) { - String path = getRealPathForUri(data.getData()); - if (path != null && !path.isEmpty()) { - nativeFolderDialogResult(userdata, path, null); - } else { - nativeFolderDialogResult( - userdata, null, "Selected folder is not available as a filesystem path"); - } - return; - } - - nativeFolderDialogResult(userdata, null, null); - } - - private String getRealPathForUri(Uri uri) { - if (uri == null) { - return null; - } - - String scheme = uri.getScheme(); - if ("file".equals(scheme)) { - return uri.getPath(); - } - - if (!"content".equals(scheme) || - !EXTERNAL_STORAGE_AUTHORITY.equals(uri.getAuthority()) || - Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) - { - return null; - } - - try { - return getExternalStoragePathForDocumentId(getExternalStorageDocumentId(uri)); - } catch (IllegalArgumentException e) { - Log.w(TAG, "Unable to resolve URI: " + uri, e); - return null; - } - } - - private static String getExternalStorageDocumentId(Uri uri) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isTreeDocumentUri(uri)) { - return DocumentsContract.getTreeDocumentId(uri); - } - - return DocumentsContract.getDocumentId(uri); - } - - private static boolean isTreeDocumentUri(Uri uri) { - List segments = uri.getPathSegments(); - return segments.size() >= 2 && "tree".equals(segments.get(0)); - } - - private String getExternalStoragePathForDocumentId(String documentId) { - if (documentId == null || documentId.isEmpty()) { - return null; - } - if (documentId.startsWith("raw:")) { - return documentId.substring("raw:".length()); - } - - String[] parts = documentId.split(":", 2); - String volumeId = parts[0]; - String relativePath = parts.length > 1 ? parts[1] : ""; - - File root = getExternalStorageRoot(volumeId); - if (root == null) { - return null; - } - - return relativePath.isEmpty() - ? root.getAbsolutePath() - : new File(root, relativePath).getAbsolutePath(); - } - - private File getExternalStorageRoot(String volumeId) { - if ("primary".equalsIgnoreCase(volumeId)) { - return Environment.getExternalStorageDirectory(); - } - if ("home".equalsIgnoreCase(volumeId)) { - return new File( - Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS); - } - - File[] externalFilesDirs = getExternalFilesDirs(null); - if (externalFilesDirs != null) { - for (File externalFilesDir : externalFilesDirs) { - File root = getStorageRootForExternalFilesDir(externalFilesDir); - if (root != null && volumeId.equalsIgnoreCase(root.getName())) { - return root; - } - } - } - - File fallback = new File("/storage", volumeId); - return fallback.exists() ? fallback : null; - } - - private File getStorageRootForExternalFilesDir(File externalFilesDir) { - if (externalFilesDir == null) { - return null; - } - - String path = externalFilesDir.getAbsolutePath(); - int androidDir = path.indexOf("/Android/"); - if (androidDir <= 0) { - return null; - } - - return new File(path.substring(0, androidDir)); - } - - private void persistUriPermissions(Intent data) { - if (data == null) { - return; - } - - int permissionFlags = - data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - if (permissionFlags == 0) { - return; - } - - Uri uri = data.getData(); - if (uri != null) { - persistUriPermission(uri, permissionFlags); - } - - ClipData clipData = data.getClipData(); - if (clipData == null) { - return; - } - for (int i = 0; i < clipData.getItemCount(); ++i) { - Uri itemUri = clipData.getItemAt(i).getUri(); - if (itemUri != null) { - persistUriPermission(itemUri, permissionFlags); - } - } - } - - private void persistUriPermission(Uri uri, int permissionFlags) { - if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) { - persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read"); - } - if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) { - persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write"); - } - } - - private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) { - try { - getContentResolver().takePersistableUriPermission(uri, permissionFlag); - } catch (SecurityException | IllegalArgumentException e) { - Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e); - } - } - - public String getDisplayNameForUri(String uriString) { - if (uriString == null || uriString.isEmpty()) { - return ""; - } - - Uri uri = Uri.parse(uriString); - if ("content".equals(uri.getScheme())) { - try (Cursor cursor = getContentResolver().query( - uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null)) - { - if (cursor != null && cursor.moveToFirst()) { - int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); - if (displayNameColumn >= 0) { - String displayName = cursor.getString(displayNameColumn); - if (displayName != null && !displayName.isEmpty()) { - return displayName; - } - } - } - } catch (SecurityException | IllegalArgumentException e) { - Log.w(TAG, "Unable to query display name for " + uri, e); - } - } else if ("file".equals(uri.getScheme())) { - String path = uri.getPath(); - if (path != null && !path.isEmpty()) { - String name = new File(path).getName(); - if (!name.isEmpty()) { - return name; - } - } - } - - String lastSegment = uri.getLastPathSegment(); - return lastSegment != null ? lastSegment : ""; - } } diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java deleted file mode 100644 index d4ed6a3041..0000000000 --- a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java +++ /dev/null @@ -1,467 +0,0 @@ -package dev.twilitrealm.dusk; - -import android.content.ContentResolver; -import android.content.res.AssetFileDescriptor; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.os.Bundle; -import android.os.CancellationSignal; -import android.os.ParcelFileDescriptor; -import android.provider.DocumentsContract; -import android.provider.DocumentsContract.Document; -import android.provider.DocumentsContract.Root; -import android.provider.DocumentsProvider; -import android.webkit.MimeTypeMap; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -public class DuskDocumentsProvider extends DocumentsProvider { - public static final String AUTHORITY = "dev.twilitrealm.dusk.documents"; - - private static final String ROOT_ID = "dusk"; - private static final String ROOT_DOCUMENT_ID = "root"; - private static final String LOCATION_DESCRIPTOR_NAME = "data_location.json"; - private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR; - - private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { - Root.COLUMN_ROOT_ID, - Root.COLUMN_FLAGS, - Root.COLUMN_TITLE, - Root.COLUMN_DOCUMENT_ID, - Root.COLUMN_ICON, - Root.COLUMN_AVAILABLE_BYTES, - Root.COLUMN_SUMMARY - }; - - private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { - Document.COLUMN_DOCUMENT_ID, - Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_FLAGS, - Document.COLUMN_MIME_TYPE, - Document.COLUMN_LAST_MODIFIED, - Document.COLUMN_SIZE - }; - - @Override - public boolean onCreate() { - if (!isCustomDataPathEnabled()) { - ensureUserDirectories(); - } - return true; - } - - @Override - public Cursor queryRoots(String[] projection) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); - if (isCustomDataPathEnabled()) { - return result; - } - - final File root = getRootDirectory(); - final MatrixCursor.RowBuilder row = result.newRow(); - - row.add(Root.COLUMN_ROOT_ID, ROOT_ID); - row.add(Root.COLUMN_FLAGS, - Root.FLAG_LOCAL_ONLY | - Root.FLAG_SUPPORTS_CREATE | - Root.FLAG_SUPPORTS_IS_CHILD); - row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name)); - row.add(Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID); - row.add(Root.COLUMN_ICON, R.mipmap.icon); - row.add(Root.COLUMN_AVAILABLE_BYTES, root.getFreeSpace()); - row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.documents_provider_summary)); - - return result; - } - - @Override - public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); - includeDocument(result, documentId, getFileForDocumentId(documentId)); - return result; - } - - @Override - public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) - throws FileNotFoundException - { - return queryChildDocumentsInternal(parentDocumentId, projection); - } - - @Override - public Cursor queryChildDocuments(String parentDocumentId, String[] projection, Bundle queryArgs) - throws FileNotFoundException - { - return queryChildDocumentsInternal(parentDocumentId, projection); - } - - private Cursor queryChildDocumentsInternal(String parentDocumentId, String[] projection) - throws FileNotFoundException - { - final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); - final File parent = getFileForDocumentId(parentDocumentId); - final File[] files = parent.listFiles(); - result.setNotificationUri(getContext().getContentResolver(), getChildDocumentsUri(parentDocumentId)); - - if (files == null) { - return result; - } - - for (File file : files) { - includeDocument(result, getDocumentIdForFile(file), file); - } - - return result; - } - - @Override - public boolean isChildDocument(String parentDocumentId, String documentId) { - try { - final File parent = getFileForDocumentId(parentDocumentId); - final File child = getFileForDocumentId(documentId); - return isInside(parent, child); - } catch (FileNotFoundException e) { - return false; - } - } - - @Override - public String createDocument(String parentDocumentId, String mimeType, String displayName) - throws FileNotFoundException - { - final File parent = getFileForDocumentId(parentDocumentId); - if (!parent.isDirectory()) { - throw new FileNotFoundException("Parent is not a directory: " + parentDocumentId); - } - - final String safeDisplayName = sanitizeDisplayName(displayName); - final File file = buildUniqueFile(parent, safeDisplayName); - final boolean created; - if (DIRECTORY_MIME_TYPE.equals(mimeType)) { - created = file.mkdir(); - } else { - try { - created = file.createNewFile(); - } catch (IOException e) { - throw asFileNotFound("Unable to create document", e); - } - } - - if (!created) { - throw new FileNotFoundException("Unable to create document: " + displayName); - } - - notifyChildrenChanged(parentDocumentId); - return getDocumentIdForFile(file); - } - - @Override - public String renameDocument(String documentId, String displayName) throws FileNotFoundException { - final File file = getFileForDocumentId(documentId); - if (ROOT_DOCUMENT_ID.equals(documentId)) { - throw new FileNotFoundException("Cannot rename root document"); - } - - final File target = buildUniqueFile(file.getParentFile(), sanitizeDisplayName(displayName)); - final String parentDocumentId = getDocumentIdForFile(file.getParentFile()); - if (!file.renameTo(target)) { - throw new FileNotFoundException("Unable to rename document: " + documentId); - } - notifyDocumentChanged(documentId); - notifyDocumentChanged(getDocumentIdForFile(target)); - notifyChildrenChanged(parentDocumentId); - return getDocumentIdForFile(target); - } - - @Override - public void deleteDocument(String documentId) throws FileNotFoundException { - if (ROOT_DOCUMENT_ID.equals(documentId)) { - throw new FileNotFoundException("Cannot delete root document"); - } - - final File file = getFileForDocumentId(documentId); - final String parentDocumentId = getDocumentIdForFile(file.getParentFile()); - deleteRecursively(file); - notifyDocumentChanged(documentId); - notifyChildrenChanged(parentDocumentId); - } - - @Override - public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) - throws FileNotFoundException - { - return ParcelFileDescriptor.open(getFileForDocumentId(documentId), modeToParcelMode(mode)); - } - - @Override - public AssetFileDescriptor openDocumentThumbnail(String documentId, android.graphics.Point sizeHint, - CancellationSignal signal) throws FileNotFoundException - { - throw new FileNotFoundException("Thumbnails are not supported"); - } - - private void includeDocument(MatrixCursor result, String documentId, File file) throws FileNotFoundException { - final MatrixCursor.RowBuilder row = result.newRow(); - final boolean isDirectory = file.isDirectory(); - final String displayName = ROOT_DOCUMENT_ID.equals(documentId) - ? getContext().getString(R.string.documents_provider_root_name) - : file.getName(); - - int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME; - if (isDirectory) { - flags |= Document.FLAG_DIR_SUPPORTS_CREATE; - } else if (file.canWrite()) { - flags |= Document.FLAG_SUPPORTS_WRITE; - } - if (ROOT_DOCUMENT_ID.equals(documentId)) { - flags &= ~(Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME); - } - - row.add(Document.COLUMN_DOCUMENT_ID, documentId); - row.add(Document.COLUMN_DISPLAY_NAME, displayName); - row.add(Document.COLUMN_FLAGS, flags); - row.add(Document.COLUMN_MIME_TYPE, isDirectory ? DIRECTORY_MIME_TYPE : getMimeType(file)); - row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified()); - row.add(Document.COLUMN_SIZE, isDirectory ? null : file.length()); - } - - private File getRootDirectory() throws FileNotFoundException { - if (isCustomDataPathEnabled()) { - throw new FileNotFoundException( - "Dusk DocumentsProvider is disabled while a custom data path is configured"); - } - - final File root = getContext().getFilesDir(); - if (root == null) { - throw new FileNotFoundException("Dusklight files directory is unavailable"); - } - return root; - } - - private File getFileForDocumentId(String documentId) throws FileNotFoundException { - final File root = getRootDirectory(); - if (ROOT_DOCUMENT_ID.equals(documentId)) { - return root; - } - if (!documentId.startsWith(ROOT_DOCUMENT_ID + "/")) { - throw new FileNotFoundException("Invalid document id: " + documentId); - } - - final String relativePath = documentId.substring(ROOT_DOCUMENT_ID.length() + 1); - final File file = new File(root, relativePath); - if (!isInside(root, file)) { - throw new FileNotFoundException("Document escapes Dusklight files directory: " + documentId); - } - if (!file.exists()) { - throw new FileNotFoundException("Document does not exist: " + documentId); - } - return file; - } - - private String getDocumentIdForFile(File file) throws FileNotFoundException { - final File root = getRootDirectory(); - if (sameFile(root, file)) { - return ROOT_DOCUMENT_ID; - } - if (!isInside(root, file)) { - throw new FileNotFoundException("File escapes Dusklight files directory: " + file); - } - - final String rootPath = canonicalPath(root); - final String filePath = canonicalPath(file); - return ROOT_DOCUMENT_ID + "/" + filePath.substring(rootPath.length() + 1); - } - - private void ensureUserDirectories() { - final File root = getContext().getFilesDir(); - if (root == null) { - return; - } - new File(root, "texture_replacements").mkdirs(); - new File(root, "USA/Card A").mkdirs(); - new File(root, "EUR/Card A").mkdirs(); - } - - private boolean isCustomDataPathEnabled() { - if (getContext() == null) { - return false; - } - - final File filesDir = getContext().getFilesDir(); - if (filesDir == null) { - return false; - } - - final File descriptor = new File(filesDir, LOCATION_DESCRIPTOR_NAME); - if (!descriptor.isFile()) { - return false; - } - - try { - final JSONObject json = new JSONObject(readText(descriptor)); - return "custom".equals(json.optString("mode", "default")); - } catch (IOException | JSONException e) { - return false; - } - } - - private static String readText(File file) throws IOException { - try (FileInputStream input = new FileInputStream(file); - ByteArrayOutputStream output = new ByteArrayOutputStream()) - { - byte[] buffer = new byte[4096]; - int bytesRead; - while ((bytesRead = input.read(buffer)) != -1) { - output.write(buffer, 0, bytesRead); - } - return output.toString(StandardCharsets.UTF_8.name()); - } - } - - private static String[] resolveRootProjection(String[] projection) { - return projection != null ? projection : DEFAULT_ROOT_PROJECTION; - } - - private static String[] resolveDocumentProjection(String[] projection) { - return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; - } - - private static String sanitizeDisplayName(String displayName) throws FileNotFoundException { - if (displayName == null) { - throw new FileNotFoundException("Document name is empty"); - } - - final String sanitized = displayName.trim(); - if (sanitized.isEmpty() || ".".equals(sanitized) || "..".equals(sanitized) || - sanitized.contains("/") || sanitized.contains("\\")) - { - throw new FileNotFoundException("Invalid document name: " + displayName); - } - return sanitized; - } - - private static File buildUniqueFile(File parent, String displayName) { - File file = new File(parent, displayName); - if (!file.exists()) { - return file; - } - - final int dot = displayName.lastIndexOf('.'); - final String baseName = dot > 0 ? displayName.substring(0, dot) : displayName; - final String extension = dot > 0 ? displayName.substring(dot) : ""; - for (int i = 1; i < 100; ++i) { - file = new File(parent, baseName + " (" + i + ")" + extension); - if (!file.exists()) { - return file; - } - } - return new File(parent, baseName + " (" + System.currentTimeMillis() + ")" + extension); - } - - private static int modeToParcelMode(String mode) { - if ("r".equals(mode)) { - return ParcelFileDescriptor.MODE_READ_ONLY; - } - if ("w".equals(mode) || "wt".equals(mode)) { - return ParcelFileDescriptor.MODE_WRITE_ONLY | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE; - } - if ("wa".equals(mode)) { - return ParcelFileDescriptor.MODE_WRITE_ONLY | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_APPEND; - } - if ("rw".equals(mode)) { - return ParcelFileDescriptor.MODE_READ_WRITE | - ParcelFileDescriptor.MODE_CREATE; - } - if ("rwt".equals(mode)) { - return ParcelFileDescriptor.MODE_READ_WRITE | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE; - } - return ParcelFileDescriptor.MODE_READ_ONLY; - } - - private static String getMimeType(File file) { - final int dot = file.getName().lastIndexOf('.'); - if (dot >= 0) { - final String extension = file.getName().substring(dot + 1).toLowerCase(); - final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - if (mimeType != null) { - return mimeType; - } - } - return "application/octet-stream"; - } - - private Uri getChildDocumentsUri(String parentDocumentId) { - return DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId); - } - - private void notifyChildrenChanged(String parentDocumentId) { - final ContentResolver resolver = getContext().getContentResolver(); - resolver.notifyChange(getChildDocumentsUri(parentDocumentId), null, false); - } - - private void notifyDocumentChanged(String documentId) { - final ContentResolver resolver = getContext().getContentResolver(); - resolver.notifyChange(DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null, false); - } - - private static void deleteRecursively(File file) throws FileNotFoundException { - if (file.isDirectory()) { - final File[] children = file.listFiles(); - if (children != null) { - for (File child : children) { - deleteRecursively(child); - } - } - } - if (!file.delete()) { - throw new FileNotFoundException("Unable to delete document: " + file); - } - } - - private static boolean isInside(File parent, File child) { - try { - final String parentPath = canonicalPath(parent); - final String childPath = canonicalPath(child); - return childPath.equals(parentPath) || childPath.startsWith(parentPath + File.separator); - } catch (FileNotFoundException e) { - return false; - } - } - - private static boolean sameFile(File a, File b) { - try { - return canonicalPath(a).equals(canonicalPath(b)); - } catch (FileNotFoundException e) { - return false; - } - } - - private static String canonicalPath(File file) throws FileNotFoundException { - try { - return file.getCanonicalPath(); - } catch (IOException e) { - throw asFileNotFound("Unable to resolve path", e); - } - } - - private static FileNotFoundException asFileNotFound(String message, IOException cause) { - final FileNotFoundException exception = new FileNotFoundException(message + ": " + cause.getMessage()); - exception.initCause(cause); - return exception; - } -} diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java deleted file mode 100644 index be160d6a2d..0000000000 --- a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java +++ /dev/null @@ -1,237 +0,0 @@ -package dev.twilitrealm.dusk; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import javax.net.ssl.HttpsURLConnection; - -public final class DuskHttpClient { - public static final int ERROR_NONE = 0; - public static final int ERROR_INVALID_URL = 1; - public static final int ERROR_UNSUPPORTED_SCHEME = 2; - public static final int ERROR_TIMEOUT = 3; - public static final int ERROR_TOO_LARGE = 4; - public static final int ERROR_NETWORK = 5; - - private static final int MAX_REDIRECTS = 5; - - public static final class Response { - public int error; - public String message; - public int statusCode; - public String[] headerNames; - public String[] headerValues; - public byte[] body; - - Response(int error, String message, int statusCode, String[] headerNames, - String[] headerValues, byte[] body) { - this.error = error; - this.message = message; - this.statusCode = statusCode; - this.headerNames = headerNames != null ? headerNames : new String[0]; - this.headerValues = headerValues != null ? headerValues : new String[0]; - this.body = body != null ? body : new byte[0]; - } - } - - private DuskHttpClient() { - } - - public static Response get(String url, String[] headerNames, String[] headerValues, - int timeoutMs, long maxBodyBytes) { - if (url == null || url.isEmpty()) { - return fail(ERROR_INVALID_URL, "URL is empty"); - } - - try { - URL currentUrl = new URL(url); - if (!isHttps(currentUrl)) { - return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported"); - } - - for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) { - HttpsURLConnection connection = - (HttpsURLConnection) currentUrl.openConnection(); - try { - connection.setRequestMethod("GET"); - connection.setConnectTimeout(timeoutMs); - connection.setReadTimeout(timeoutMs); - connection.setUseCaches(false); - connection.setInstanceFollowRedirects(false); - applyHeaders(connection, headerNames, headerValues); - - int statusCode = connection.getResponseCode(); - if (isRedirect(statusCode)) { - String location = connection.getHeaderField("Location"); - if (location == null || location.isEmpty()) { - return fail(ERROR_NETWORK, "Redirect response did not include Location", - statusCode, connection, new byte[0]); - } - - URL nextUrl = new URL(currentUrl, location); - if (!isHttps(nextUrl)) { - return fail(ERROR_UNSUPPORTED_SCHEME, - "Only https:// redirects are supported", statusCode, - connection, new byte[0]); - } - currentUrl = nextUrl; - continue; - } - - byte[] body = readBody(connection, statusCode, maxBodyBytes); - return success(statusCode, connection, body); - } catch (ResponseTooLargeException e) { - return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit", - safeStatusCode(connection), connection, e.partialBody); - } finally { - connection.disconnect(); - } - } - - return fail(ERROR_NETWORK, "Too many redirects"); - } catch (MalformedURLException e) { - return fail(ERROR_INVALID_URL, "Failed to parse URL"); - } catch (SocketTimeoutException e) { - return fail(ERROR_TIMEOUT, "Request timed out"); - } catch (IOException e) { - String message = e.getMessage(); - return fail(ERROR_NETWORK, message != null ? message : e.toString()); - } catch (ClassCastException e) { - return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported"); - } - } - - private static void applyHeaders(HttpsURLConnection connection, String[] names, - String[] values) { - if (names == null || values == null) { - return; - } - - int count = Math.min(names.length, values.length); - for (int i = 0; i < count; ++i) { - if (names[i] != null && values[i] != null) { - connection.setRequestProperty(names[i], values[i]); - } - } - } - - private static boolean isHttps(URL url) { - return "https".equalsIgnoreCase(url.getProtocol()); - } - - private static boolean isRedirect(int statusCode) { - return statusCode == HttpURLConnection.HTTP_MOVED_PERM || - statusCode == HttpURLConnection.HTTP_MOVED_TEMP || - statusCode == HttpURLConnection.HTTP_SEE_OTHER || - statusCode == 307 || - statusCode == 308; - } - - private static byte[] readBody(HttpsURLConnection connection, int statusCode, - long maxBodyBytes) throws IOException, - ResponseTooLargeException { - InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ? - connection.getErrorStream() : connection.getInputStream(); - if (stream == null) { - return new byte[0]; - } - - try (InputStream bodyStream = stream; - ByteArrayOutputStream out = new ByteArrayOutputStream()) { - byte[] buffer = new byte[8192]; - long total = 0; - while (true) { - int read = bodyStream.read(buffer); - if (read < 0) { - return out.toByteArray(); - } - if (read == 0) { - continue; - } - if (read > maxBodyBytes || total > maxBodyBytes - read) { - throw new ResponseTooLargeException(out.toByteArray()); - } - out.write(buffer, 0, read); - total += read; - } - } - } - - private static int safeStatusCode(HttpsURLConnection connection) { - try { - return connection.getResponseCode(); - } catch (IOException e) { - return 0; - } - } - - private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) { - HeaderLists headers = readHeaders(connection); - return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body); - } - - private static Response fail(int error, String message) { - return new Response(error, message, 0, null, null, null); - } - - private static Response fail(int error, String message, int statusCode, - HttpsURLConnection connection, byte[] body) { - HeaderLists headers = readHeaders(connection); - return new Response(error, message, statusCode, headers.names, headers.values, body); - } - - private static HeaderLists readHeaders(HttpsURLConnection connection) { - List names = new ArrayList<>(); - List values = new ArrayList<>(); - - Map> headerFields = connection.getHeaderFields(); - if (headerFields == null) { - return new HeaderLists(new String[0], new String[0]); - } - - for (Map.Entry> entry : headerFields.entrySet()) { - String name = entry.getKey(); - if (name == null) { - continue; - } - List entryValues = entry.getValue(); - if (entryValues == null || entryValues.isEmpty()) { - names.add(name); - values.add(""); - continue; - } - for (String value : entryValues) { - names.add(name); - values.add(value != null ? value : ""); - } - } - - return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0])); - } - - private static final class HeaderLists { - final String[] names; - final String[] values; - - HeaderLists(String[] names, String[] values) { - this.names = names; - this.values = values; - } - } - - private static final class ResponseTooLargeException extends Exception { - final byte[] partialBody; - - ResponseTooLargeException(byte[] partialBody) { - this.partialBody = partialBody; - } - } -} diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDevice.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDevice.java deleted file mode 100644 index f96095324b..0000000000 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDevice.java +++ /dev/null @@ -1,21 +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 writeReport(byte[] report, boolean feature); - public boolean readReport(byte[] report, boolean feature); - public void setFrozen(boolean frozen); - public void close(); - public void shutdown(); -} diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java deleted file mode 100644 index bcd8806c41..0000000000 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java +++ /dev/null @@ -1,829 +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.HashMap; -import java.util.LinkedList; -import java.util.UUID; - -import java.util.regex.Pattern; -import java.util.regex.Matcher; - -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 mHasEnabledNotifications = false; - private boolean mHasSeenInputUpdate = false; - private boolean mFrozen = false; - private LinkedList mOperations; - GattOperation mCurrentOperation = null; - private Handler mHandler; - private int mProductId = -1; - private int mReportId = 0; - private UUID mInputCharacteristic; - - private static final int D0G_BLE2_PID = 0x1106; - private static final int TRITON_BLE_PID = 0x1303; - - - 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 final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); - static final UUID inputCharacteristicD0G = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); - static final UUID inputCharacteristicTriton_0x45 = UUID.fromString("100F6C7A-1735-4313-B402-38567131E5F3"); - static final UUID inputCharacteristicTriton_0x47 = UUID.fromString("100F6C7C-1735-4313-B402-38567131E5F3"); - static 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 }; - - private HashMap mOutputReportChars = new HashMap(); - - static class GattOperation { - private enum Operation { - CHR_READ, - CHR_WRITE, - ENABLE_NOTIFICATION - } - - Operation mOp; - UUID mUuid; - byte[] mValue; - BluetoothGatt mGatt; - boolean mResult = true; - int mDelayMs = 0; - - private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { - mGatt = gatt; - mOp = operation; - mUuid = uuid; - } - - private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, int delayMs) { - mGatt = gatt; - mOp = operation; - mUuid = uuid; - mDelayMs = delayMs; - } - - private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { - mGatt = gatt; - mOp = operation; - mUuid = uuid; - mValue = value; - } - - private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value, int delayMs) { - mGatt = gatt; - mOp = operation; - mUuid = uuid; - mValue = value; - mDelayMs = delayMs; - } - - 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; - } - - public int getDelayMs() { return mDelayMs; } - - 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); - } - - static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid, int delayMs) { - return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid, delayMs); - } - } - - HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { - mManager = manager; - mDevice = device; - mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier()); - mIsRegistered = false; - mIsChromebook = SDLActivity.isChromebook(); - mOperations = new LinkedList(); - mHandler = new Handler(Looper.getMainLooper()); - - mGatt = connectGatt(); - mHasEnabledNotifications = false; - mHasSeenInputUpdate = false; - // final HIDDeviceBLESteamController finalThis = this; - // mHandler.postDelayed(new Runnable() { - // @Override - // void run() { - // finalThis.checkConnectionForChromebookIssue(); - // } - // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); - } - - String getIdentifier() { - return String.format("SteamController.%s", mDevice.getAddress()); - } - - 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); - } - - 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(inputCharacteristicTriton_0x45)) { - Log.v(TAG, "Found Triton input characteristic 0x45"); - mProductId = TRITON_BLE_PID; - mReportId = 0x45; - mInputCharacteristic = chr.getUuid(); - } else if (chr.getUuid().equals(inputCharacteristicTriton_0x47)) { - Log.v(TAG, "Found Triton input characteristic 0x47"); - mProductId = TRITON_BLE_PID; - mReportId = 0x47; - mInputCharacteristic = chr.getUuid(); - } else if (chr.getUuid().equals(inputCharacteristicD0G)) { - Log.v(TAG, "Found D0G input characteristic"); - mProductId = D0G_BLE2_PID; - mReportId = 0x03; - mInputCharacteristic = chr.getUuid(); - } else { - Pattern reportPattern = Pattern.compile("100F6C([0-9A-Z]{2})", Pattern.CASE_INSENSITIVE); - Matcher matcher = reportPattern.matcher(chr.getUuid().toString()); - - if (matcher.find()) { - try { - int reportId = Integer.parseInt(matcher.group(1), 16); - - reportId -= 0x35; - if (reportId >= 0x80) { - // This is a Triton output report characteristic that we need to care about. - Log.v(TAG, "Found Triton output report 0x" + Integer.toString(reportId, 16)); - mOutputReportChars.put(reportId, chr); - } - } - catch (NumberFormatException nfe) { - Log.w(TAG, "Could not parse report characteristic " + chr.getUuid().toString() + ": " + nfe.toString()); - } - } - } - } - - for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { - if (chr.getUuid().equals(mInputCharacteristic)) { - // 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(); - } - - Runnable gattOperationRunnable = 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 - } - } - }; - - if (mCurrentOperation.getDelayMs() == 0) { - // Run in main thread - mHandler.post(gattOperationRunnable); - } - else { - // If we have a delay on this operation, wait before we post it. - mHandler.postDelayed(gattOperationRunnable, mCurrentOperation.getDelayMs()); - } - - } - - private void queueGattOperation(GattOperation op) { - synchronized (mOperations) { - mOperations.add(op); - } - executeNextGattOperation(); - } - - private void enableNotification(UUID chrUuid) { - // Add a 500ms delay to notification write for Amazon Fire TV devices, as otherwise if we do this too quickly after connecting - // it will return success and then silently drop the operation on the floor. - GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid, 500); - queueGattOperation(op); - - // Amazon Fire devices can also silently timeout on writeDescriptor, so - // set up a little delayed check that will attempt to write a second time. - // - // While this only seems to be needed on Amazon Fire TV devices at present, it - // doesn't hurt to have a retry on other devices as well. - // - final HIDDeviceBLESteamController finalThis = this; - final UUID finalUuid = chrUuid; - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - if (!finalThis.mHasEnabledNotifications) { - - if (finalThis.mHasSeenInputUpdate) { - // Amazon Five devices may have enabled notifications on the input characteristic and not given us a callback. If we've seen - // input reports, though, somewhat by definition notifications are enabled. - Log.w(TAG, "WriteDescriptor has never returned, but we've seen input reports. Moving on with controller initialization."); - finalThis.mHasEnabledNotifications = true; - finalThis.enableValveMode(); - return; - } - - // Give one more try. - GattOperation retry = HIDDeviceBLESteamController.GattOperation.enableNotification(finalThis.mGatt, finalUuid, 500); - finalThis.queueGattOperation(retry); - } - } - }, 1000); - } - - void writeCharacteristic(UUID uuid, byte[] value) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value); - queueGattOperation(op); - } - - void readCharacteristic(UUID uuid) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid); - queueGattOperation(op); - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////// BluetoothGattCallback overridden methods - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - 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. - } - - @Override - 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 { - if (getProductId() == TRITON_BLE_PID) { - // Android will not properly play well with Data Length Extensions without manually requesting a large MTU, - // and Triton controllers require DLE support. - // - // 517 is basically a "magic number" as far as Android's bluetooth code is concerned, so do not change - // this value. It is functionally "please enable data length extensions" on some Android builds. - mGatt.requestMtu(517); - } - - probeService(this); - } - } - } - - @Override - 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.HIDDeviceReportResponse(getId(), characteristic.getValue()); - } - - finishCurrentGattOperation(); - } - - @Override - 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, true, mReportId); - setRegistered(); - } - } - - finishCurrentGattOperation(); - } - - @Override - 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(mInputCharacteristic) && !mFrozen) { - mHasSeenInputUpdate = true; - mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); - } - } - - @Override - public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - //Log.v(TAG, "onDescriptorRead status=" + status); - } - - private void enableValveMode() - { - BluetoothGattService valveService = mGatt.getService(steamControllerService); - if (valveService == null) - return; - - BluetoothGattCharacteristic reportChr = valveService.getCharacteristic(reportCharacteristic); - if (reportChr != null) { - if (getProductId() == TRITON_BLE_PID) { - // For Triton we just mark things registered. - Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId()); - mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId); - setRegistered(); - } else { - // For the original controller, we need to manually enter Valve mode. - Log.v(TAG, "Writing report characteristic to enter valve mode"); - reportChr.setValue(enterValveMode); - mGatt.writeCharacteristic(reportChr); - } - } - } - - @Override - 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(mInputCharacteristic)) { - mHasEnabledNotifications = true; - enableValveMode(); - } - - finishCurrentGattOperation(); - } - - @Override - public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { - //Log.v(TAG, "onReliableWriteCompleted status=" + status); - } - - @Override - public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { - //Log.v(TAG, "onReadRemoteRssi status=" + status); - } - - @Override - 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() { - if (mProductId > 0) { - // We've already set a product ID. - return mProductId; - } - - if (mDevice.getName().startsWith("Steam Ctrl")) { - // We're a newer Triton device - mProductId = TRITON_BLE_PID; - } else { - // We're an OG Steam Controller - mProductId = D0G_BLE2_PID; - } - - return mProductId; - } - - @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 writeReport(byte[] report, boolean feature) { - if (!isRegistered()) { - Log.e(TAG, "Attempted writeReport before Steam Controller is registered!"); - if (mIsConnected) { - probeService(this); - } - return -1; - } - - if (feature) { - // 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, "writeFeatureReport " + HexDump.dumpHexString(actual_report)); - writeCharacteristic(reportCharacteristic, actual_report); - return report.length; - } else { - // If we're an original-recipe Steam Controller we just write to the characteristic directly. - if (getProductId() == D0G_BLE2_PID) { - //Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report)); - writeCharacteristic(reportCharacteristic, report); - return report.length; - } - - // If we're a Triton, we need to find the correct report characteristic. - if (report.length > 0) { - int reportId = report[0] & 0xFF; - BluetoothGattCharacteristic targetedReportCharacteristic = mOutputReportChars.get(reportId); - if (targetedReportCharacteristic != null) { - byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); - //Log.v(TAG, "writeOutputReport 0x" + Integer.toString(reportId, 16) + " " + HexDump.dumpHexString(report)); - writeCharacteristic(targetedReportCharacteristic.getUuid(), actual_report); - return report.length; - } else { - Log.w(TAG, "Got report write request for unknown report type 0x" + Integer.toString(reportId, 16)); - } - } - } - - return -1; - } - - @Override - public boolean readReport(byte[] report, boolean feature) { - if (!isRegistered()) { - Log.e(TAG, "Attempted readReport before Steam Controller is registered!"); - if (mIsConnected) { - probeService(this); - } - return false; - } - - if (feature) { - readCharacteristic(reportCharacteristic); - return true; - } else { - // Not implemented - return false; - } - } - - @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/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java deleted file mode 100644 index 691416c1c9..0000000000 --- a/platforms/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; - - static public HIDDeviceManager acquire(Context context) { - if (sManagerRefCount == 0) { - sManager = new HIDDeviceManager(context); - } - ++sManagerRefCount; - return sManager; - } - - static public 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 = SDLActivity.isChromebook(); - -// if (shouldClear) { -// SharedPreferences.Editor spedit = mSharedPreferences.edit(); -// spedit.clear(); -// spedit.apply(); -// } -// else - { - mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); - } - } - - Context getContext() { - return mContext; - } - - 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.apply(); - 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); - if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */ - mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED); - } else { - 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 - 0x3537, // GameSir - 0x37d7, // Flydigi - 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 - 0x294b, // Snakebyte - 0x2dc8, // 8BitDo - 0x2e24, // Hyperkin - 0x2e95, // SCUF - 0x3285, // Nacon - 0x3537, // GameSir - 0x366c, // ByoWave - }; - - 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(), false, 0); - } - } - } - } - - 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)) { - 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); - if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */ - mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED); - } else { - 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. - 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); - } - - 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; - } - - 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); - } - } - - 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; - } - - // Steam Controllers will always support Bluetooth Low Energy - if ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) == 0) { - return false; - } - - // Match on the name either the original Steam Controller or the new second-generation one advertise with. - return bluetoothDevice.getName().equals("SteamController") || bluetoothDevice.getName().startsWith("Steam Ctrl"); - } - - 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 - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - boolean initialize(boolean usb, boolean bluetooth) { - Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")"); - - if (usb) { - initializeUSB(); - } - if (bluetooth) { - initializeBluetooth(); - } - return true; - } - - 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; - } - - Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION); - intent.setPackage(mContext.getPackageName()); - mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, 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; - } - - int writeReport(int deviceID, byte[] report, boolean feature) { - try { - //Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return -1; - } - - return device.writeReport(report, feature); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return -1; - } - - boolean readReport(int deviceID, byte[] report, boolean feature) { - try { - //Log.v(TAG, "readReport deviceID=" + deviceID); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return false; - } - - return device.readReport(report, feature); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return false; - } - - 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, boolean bBluetooth, int reportID); - 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 HIDDeviceReportResponse(int deviceID, byte[] report); -} diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java deleted file mode 100644 index 8954639733..0000000000 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java +++ /dev/null @@ -1,354 +0,0 @@ -package org.libsdl.app; - -import android.hardware.usb.*; -import android.os.Build; -import android.util.Log; -import java.util.Arrays; -import java.util.Locale; - -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; - protected boolean mClaimed; - - 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; - mClaimed = false; - } - - String getIdentifier() { - return String.format(Locale.ENGLISH, "%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; - 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; - result = mDevice.getManufacturerName(); - if (result == null) { - result = String.format("%x", getVendorId()); - } - return result; - } - - @Override - public String getProductName() { - String result; - result = mDevice.getProductName(); - if (result == null) { - result = String.format("%x", getProductId()); - } - return result; - } - - @Override - public UsbDevice getDevice() { - return mDevice; - } - - 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; - } - mClaimed = true; - - // 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. The original Steam Controller and the wireless dongle for it do NOT - // actually have -- or require -- output endpoints, so we need to accept only an input one for them or else we'll fall - // back to the Android system gamepad functionality (and lose our paddles et al). - if (mInputEndpoint == null) { - Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); - mConnection.releaseInterface(iface); - close(); - return false; - } - - // Start listening for input - mRunning = true; - mInputThread = new InputThread(); - mInputThread.start(); - - return true; - } - - @Override - public int writeReport(byte[] report, boolean feature) { - if (mConnection == null) { - Log.w(TAG, "writeReport() called with no device connection"); - return -1; - } - - if (!mClaimed) { - Log.w(TAG, "writeReport() called but some other process currently owns the USB device"); - return -1; - } - - if (feature) { - 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, "writeFeatureReport() returned " + res + " on device " + getDeviceName()); - return -1; - } - - if (skipped_report_id) { - ++length; - } - return length; - } else { - if (mOutputEndpoint == null) - { - Log.e(TAG, "Tried to write an output report to an interface with no output endpoint!"); - return -1; - } - int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); - if (res != report.length) { - Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName()); - } - return res; - } - } - - @Override - public boolean readReport(byte[] report, boolean feature) { - int res = -1; - int offset = 0; - int length = report.length; - boolean skipped_report_id = false; - byte report_number = report[0]; - - if (mConnection == null) { - Log.w(TAG, "readReport() called with no device connection"); - return false; - } - if (!mClaimed) { - if (feature) { - return false; - } - return true; - } - - 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*/, - ((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 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.HIDDeviceReportResponse(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) { - if (mClaimed) { - UsbInterface iface = mDevice.getInterface(mInterfaceIndex); - mConnection.releaseInterface(iface); - } - mConnection.close(); - mConnection = null; - mClaimed = false; - } - } - - @Override - public void shutdown() { - close(); - mManager = null; - } - - @Override - public void setFrozen(boolean frozen) { - mFrozen = frozen; - - /* If we have a valid device connection and the claim state doesn't match what we want, try to correct that. */ - if (mConnection != null && mClaimed == mFrozen) { - UsbInterface iface = mDevice.getInterface(mInterfaceIndex); - if (frozen) { - mClaimed = !mConnection.releaseInterface(iface); - if (mClaimed) { - Log.e(TAG, "Tried to release claim on USB device, but failed!"); - } - } else { - mClaimed = mConnection.claimInterface(iface, true); - if (!mClaimed) { - Log.e(TAG, "Tried to regain claim on USB device, but failed!"); - } - } - } - } - - 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/platforms/android/app/src/main/java/org/libsdl/app/SDL.java b/platforms/android/app/src/main/java/org/libsdl/app/SDL.java deleted file mode 100644 index d9650a72e4..0000000000 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDL.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.libsdl.app; - -import android.app.Activity; -import android.content.Context; - -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 - static public void setupJNI() { - SDLActivity.nativeSetupJNI(); - SDLAudioManager.nativeSetupJNI(); - SDLControllerManager.nativeSetupJNI(); - } - - // This function should be called each time the activity is started - static public void initialize() { - setContext(null); - - SDLActivity.initialize(); - SDLAudioManager.initialize(); - SDLControllerManager.initialize(); - } - - // This function stores the current activity (SDL or not) - static public void setContext(Activity context) { - SDLAudioManager.setContext(context); - mContext = context; - } - - static public Activity getContext() { - return mContext; - } - - static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { - loadLibrary(libraryName, mContext); - } - - 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 Activity mContext; -} diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java deleted file mode 100644 index dcc49852ac..0000000000 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ /dev/null @@ -1,2240 +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.ActivityNotFoundException; -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.LocaleList; -import android.os.Message; -import android.os.ParcelFileDescriptor; -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.InputConnection; -import android.view.inputmethod.InputMethodManager; -import android.webkit.MimeTypeMap; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; - -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.Locale; - - -/** - SDL Activity -*/ -public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { - private static final String TAG = "SDL"; - private static final int SDL_MAJOR_VERSION = 3; - private static final int SDL_MINOR_VERSION = 4; - 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; - - 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; - - 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; - private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPLEFT = 12; - private static final int SDL_SYSTEM_CURSOR_WINDOW_TOP = 13; - private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPRIGHT = 14; - private static final int SDL_SYSTEM_CURSOR_WINDOW_RIGHT = 15; - private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMRIGHT = 16; - private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOM = 17; - private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMLEFT = 18; - private static final int SDL_SYSTEM_CURSOR_WINDOW_LEFT = 19; - - 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 mCurrentRotation; - 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 SDLDummyEdit mTextEdit; - protected static ViewGroup mLayout; - protected static SDLClipboardHandler mClipboardHandler; - protected static Hashtable mCursors; - protected static int mLastCursorID; - protected static SDLGenericMotionListener_API14 mMotionListener; - protected static HIDDeviceManager mHIDDeviceManager; - - // This is what SDL runs in. It invokes SDL_main(), eventually - protected static Thread mSDLThread; - protected static boolean mSDLMainFinished = false; - protected static boolean mActivityCreated = false; - private static SDLFileDialogState mFileDialogState = null; - protected static boolean mDispatchingKeyEvent = false; - - public static SDLGenericMotionListener_API14 getMotionListener() { - if (mMotionListener == null) { - if (Build.VERSION.SDK_INT >= 29 /* Android 10 (Q) */) { - mMotionListener = new SDLGenericMotionListener_API29(); - } else 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_API14(); - } - } - - return mMotionListener; - } - - /** - * The application entry point, called on a dedicated thread (SDLThread). - * The default implementation uses the getMainSharedObject() and getMainFunction() methods - * to invoke native code from the specified shared library. - * It can be overridden by derived classes. - */ - protected void main() { - String library = SDLActivity.mSingleton.getMainSharedObject(); - String function = SDLActivity.mSingleton.getMainFunction(); - String[] arguments = SDLActivity.mSingleton.getArguments(); - - Log.v("SDL", "Running main function " + function + " from library " + library); - SDLActivity.nativeRunMain(library, function, arguments); - Log.v("SDL", "Finished main function"); - } - - /** - * 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 "SDL3". - * Also keep in mind that the order the libraries are loaded may matter. - * @return names of shared libraries to be loaded (e.g. "SDL3", "main"). - */ - protected String[] getLibraries() { - return new String[] { - "SDL3", - // "SDL3_image", - // "SDL3_mixer", - // "SDL3_net", - // "SDL3_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, "Manufacturer: " + Build.MANUFACTURER); - Log.v(TAG, "Device: " + Build.DEVICE); - Log.v(TAG, "Model: " + Build.MODEL); - Log.v(TAG, "onCreate()"); - super.onCreate(savedInstanceState); - - - /* Control activity re-creation */ - if (mSDLMainFinished || mActivityCreated) { - boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity(); - if (mSDLMainFinished) { - Log.v(TAG, "SDL main() finished"); - } - if (allow_recreate) { - Log.v(TAG, "activity re-created"); - } else { - Log.v(TAG, "activity finished"); - System.exit(0); - return; - } - } - - mActivityCreated = true; - - 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; - } - - - /* Control activity re-creation */ - /* Robustness: check that the native code is run for the first time. - * (Maybe Activity was reset, but not the native code.) */ - { - int run_count = SDLActivity.nativeCheckSDLThreadCounter(); /* get and increment a native counter */ - if (run_count != 0) { - boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity(); - if (allow_recreate) { - Log.v(TAG, "activity re-created // run_count: " + run_count); - } else { - Log.v(TAG, "activity finished // run_count: " + run_count); - System.exit(0); - 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. - SDLActivity.nativeSetNaturalOrientation(SDLActivity.getNaturalOrientation()); - mCurrentRotation = SDLActivity.getCurrentRotation(); - SDLActivity.onNativeRotationChanged(mCurrentRotation); - - 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) { - } - - switch (getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) { - case Configuration.UI_MODE_NIGHT_NO: - SDLActivity.onNativeDarkModeChanged(false); - break; - case Configuration.UI_MODE_NIGHT_YES: - SDLActivity.onNativeDarkModeChanged(true); - break; - } - - 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 getNaturalOrientation() { - int result = SDL_ORIENTATION_UNKNOWN; - - Activity activity = getContext(); - if (activity != null) { - Configuration config = activity.getResources().getConfiguration(); - Display display = activity.getWindowManager().getDefaultDisplay(); - int rotation = display.getRotation(); - if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) && - config.orientation == Configuration.ORIENTATION_LANDSCAPE) || - ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) && - config.orientation == Configuration.ORIENTATION_PORTRAIT)) { - result = SDL_ORIENTATION_LANDSCAPE; - } else { - result = SDL_ORIENTATION_PORTRAIT; - } - } - return result; - } - - public static int getCurrentRotation() { - int result = 0; - - Activity activity = getContext(); - if (activity != null) { - Display display = activity.getWindowManager().getDefaultDisplay(); - switch (display.getRotation()) { - case Surface.ROTATION_0: - result = 0; - break; - case Surface.ROTATION_90: - result = 90; - break; - case Surface.ROTATION_180: - result = 180; - break; - case Surface.ROTATION_270: - result = 270; - break; - } - } - return result; - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); - - // If we are gaining focus, we can always try to restore our USB devices. If we are losing focus, - // only try to relinquish them if we don't have background events allowed (for multi-window Android setups). - if (hasFocus || !SDLActivity.nativeGetHintBoolean("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", false)) { - if (mHIDDeviceManager != null) { - mHIDDeviceManager.setFrozen(!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 onTrimMemory(int level) { - Log.v(TAG, "onTrimMemory()"); - super.onTrimMemory(level); - - 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(); - } - - switch (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) { - case Configuration.UI_MODE_NIGHT_NO: - SDLActivity.onNativeDarkModeChanged(false); - break; - case Configuration.UI_MODE_NIGHT_YES: - SDLActivity.onNativeDarkModeChanged(true); - break; - } - } - - @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 { - // Use a timeout because: - // C SDLmain() thread might have started (mSDLThread.start() called) - // while the SDL_Init() might not have been called yet, - // and so the previous QUIT event will be discarded by SDL_Init() and app is running, not exiting. - SDLActivity.mSDLThread.join(1000); - } 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(); - } - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (mFileDialogState != null && mFileDialogState.requestCode == requestCode) { - /* This is our file dialog */ - String[] filelist = null; - - if (data != null) { - Uri singleFileUri = data.getData(); - - if (singleFileUri == null) { - /* Use Intent.getClipData to get multiple choices */ - ClipData clipData = data.getClipData(); - assert clipData != null; - - filelist = new String[clipData.getItemCount()]; - - for (int i = 0; i < filelist.length; i++) { - String uri = clipData.getItemAt(i).getUri().toString(); - filelist[i] = uri; - } - } else { - /* Only one file is selected. */ - filelist = new String[]{singleFileUri.toString()}; - } - } else { - /* User cancelled the request. */ - filelist = new String[0]; - } - - // TODO: Detect the file MIME type and pass the filter value accordingly. - SDLActivity.onNativeFileDialog(requestCode, filelist, -1); - mFileDialogState = null; - } - } - - // 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; - } - mDispatchingKeyEvent = true; - boolean result = super.dispatchKeyEvent(event); - mDispatchingKeyEvent = false; - return result; - } - - public static boolean dispatchingKeyEvent() { - return mDispatchingKeyEvent; - } - - /* 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 || mHasMultiWindow) && 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 - protected static final int COMMAND_CHANGE_TITLE = 1; - protected static final int COMMAND_CHANGE_WINDOW_STYLE = 2; - protected static final int COMMAND_TEXTEDIT_HIDE = 3; - protected 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 = 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 (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 >= 30 /* Android 11 (R) */) { - window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - } - if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */ && - Build.VERSION.SDK_INT < 35 /* Android 15 */) { - SDLActivity.onNativeInsetsChanged(0, 0, 0, 0); - } - } - } 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); - - onNativeScreenKeyboardHidden(); - - 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 - protected boolean sendCommand(int command, Object data) { - Message msg = commandHandler.obtainMessage(); - msg.arg1 = command; - msg.obj = data; - boolean result = commandHandler.sendMessage(msg); - - 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 && (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 (getContext()) { - try { - getContext().wait(500); - } catch (InterruptedException ie) { - ie.printStackTrace(); - } - } - } - } - - return result; - } - - // C functions we call - public static native String nativeGetVersion(); - public static native void nativeSetupJNI(); - public static native void nativeInitMainThread(); - public static native void nativeCleanupMainThread(); - 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 density, 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 onNativePen(int penId, int device_type, int button, 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 void onNativeScreenKeyboardShown(); - public static native void onNativeScreenKeyboardHidden(); - 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 nativeSetNaturalOrientation(int orientation); - public static native void onNativeRotationChanged(int rotation); - public static native void onNativeInsetsChanged(int left, int right, int top, int bottom); - public static native void nativeAddTouch(int touchId, String name); - public static native void nativePermissionResult(int requestCode, boolean result); - public static native void onNativeLocaleChanged(); - public static native void onNativeDarkModeChanged(boolean enabled); - public static native boolean nativeAllowRecreateActivity(); - public static native int nativeCheckSDLThreadCounter(); - public static native void onNativeFileDialog(int requestCode, String[] filelist, int filter); - public static native void onNativePinchStart(); - public static native void onNativePinchUpdate(float scale); - public static native void onNativePinchEnd(); - - /** - * 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 (w <= 1 || h <= 1) { - // Invalid width/height, ignore this request - return; - } - - /* If set, hint "explicitly controls which UI orientations are allowed". */ - if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { - orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_USER_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_USER_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() { - return false; - } - - /** - * 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 Activity 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.startsWith("TV") || - Build.MODEL.equals("X96-W") || - Build.MODEL.equals("A95X-R1"))) { - return true; - } - return false; - } - - public static boolean isVRHeadset() { - if (Build.MANUFACTURER.equals("Oculus") && Build.MODEL.startsWith("Quest")) { - return true; - } - if (Build.MANUFACTURER.equals("Pico")) { - return true; - } - return false; - } - - public static double getDiagonal() - { - DisplayMetrics metrics = new DisplayMetrics(); - 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() { - // https://stackoverflow.com/questions/39784415/how-to-detect-programmatically-if-android-app-is-running-in-chrome-book-or-in - if (getContext() != null) { - if (getContext().getPackageManager().hasSystemFeature("org.chromium.arc") - || getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management")) { - return true; - } - } - - // Running on AVD emulator - boolean isChromebookEmulator = (Build.MODEL != null && Build.MODEL.startsWith("sdk_gpc_")); - return isChromebookEmulator; - } - - /** - * 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 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 input_type; - public int x, y, w, h; - - public ShowTextInputTask(int input_type, int x, int y, int w, int h) { - this.input_type = input_type; - 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 SDLDummyEdit(getContext()); - - mLayout.addView(mTextEdit, params); - } else { - mTextEdit.setLayoutParams(params); - } - mTextEdit.setInputType(input_type); - - mTextEdit.setVisibility(View.VISIBLE); - mTextEdit.requestFocus(); - - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mTextEdit, 0); - - if (imm.isAcceptingText()) { - onNativeScreenKeyboardShown(); - } - } - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean showTextInput(int input_type, int x, int y, int w, int h) { - // Transfer the task to the main thread as a Runnable - return mSingleton.commandHandler.post(new ShowTextInputTask(input_type, 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, event.getScanCode())) { - return true; - } - } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (SDLControllerManager.onNativePadUp(deviceId, keyCode, event.getScanCode())) { - return true; - } - } - } - - if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { - if (SDLActivity.isVRHeadset()) { - // The Oculus Quest controller back button comes in as source mouse, so accept that - } else { - // 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) { - onNativeKeyDown(keyCode); - - if (isTextInputEvent(event)) { - if (ic != null) { - ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); - } else { - SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); - } - } - 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())) { - - nativeAddTouch(device.getId(), 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