Просмотр исходного кода

cmake: create Android jars + apks for tests

Anonymous Maarten 2 лет назад
Родитель
Сommit
a4bb4eef73
30 измененных файлов с 1370 добавлено и 3 удалено
  1. 16 1
      .github/workflows/android.yml
  2. 67 2
      CMakeLists.txt
  3. 103 0
      cmake/android/FindSdlAndroid.cmake
  4. 115 0
      cmake/android/FindSdlAndroidBuildTools.cmake
  5. 123 0
      cmake/android/FindSdlAndroidPlatform.cmake
  6. 276 0
      cmake/android/SdlAndroidFunctions.cmake
  7. 74 0
      cmake/android/SdlAndroidScript.cmake
  8. 76 0
      docs/README-android.md
  9. 135 0
      test/CMakeLists.txt
  10. 70 0
      test/android/cmake/AndroidManifest.xml.cmake
  11. 121 0
      test/android/cmake/SDLEntryTestActivity.java.cmake
  12. 33 0
      test/android/cmake/SDLTestActivity.java.cmake
  13. 5 0
      test/android/cmake/res/values/strings.xml.cmake
  14. 24 0
      test/android/cmake/res/xml/shortcuts.xml.cmake
  15. 85 0
      test/android/res/drawable/sdl-test_foreground.xml
  16. 26 0
      test/android/res/layout/arguments_layout.xml
  17. 5 0
      test/android/res/mipmap-anydpi-v26/sdl-test.xml
  18. 5 0
      test/android/res/mipmap-anydpi-v26/sdl-test_round.xml
  19. BIN
      test/android/res/mipmap-hdpi/sdl-test.png
  20. BIN
      test/android/res/mipmap-hdpi/sdl-test_round.png
  21. BIN
      test/android/res/mipmap-mdpi/sdl-test.png
  22. BIN
      test/android/res/mipmap-mdpi/sdl-test_round.png
  23. BIN
      test/android/res/mipmap-xhdpi/sdl-test.png
  24. BIN
      test/android/res/mipmap-xhdpi/sdl-test_round.png
  25. BIN
      test/android/res/mipmap-xxhdpi/sdl-test.png
  26. BIN
      test/android/res/mipmap-xxhdpi/sdl-test_round.png
  27. BIN
      test/android/res/mipmap-xxxhdpi/sdl-test.png
  28. BIN
      test/android/res/mipmap-xxxhdpi/sdl-test_round.png
  29. 7 0
      test/android/res/values/arg_strings.xml
  30. 4 0
      test/android/res/values/sdl-test_background.xml

+ 16 - 1
.github/workflows/android.yml

@@ -16,7 +16,7 @@ jobs:
       matrix:
         platform:
           - { name: Android.mk  }
-          - { name: CMake, cmake: 1, android_abi: "arm64-v8a", android_platform: 23, arch: "aarch64", artifact: "SDL-android-arm64" }
+          - { name: CMake, cmake: 1, android_abi: "arm64-v8a", android_platform: 23, arch: "aarch64", artifact: "SDL-android-arm64", apk-artifact: "SDL-android-apks-arm64" }
 
     steps:
       - uses: actions/checkout@v3
@@ -28,6 +28,11 @@ jobs:
         if: ${{ matrix.platform.name == 'Android.mk' }}
         run: |
           ./build-scripts/androidbuildlibs.sh
+      - uses: actions/setup-java@v3
+        if: ${{ matrix.platform.name == 'CMake' }}
+        with:
+          distribution: 'temurin'
+          java-version: '11'
       - name: Setup (CMake)
         if: ${{ matrix.platform.name == 'CMake' }}
         run: |
@@ -56,6 +61,10 @@ jobs:
         if: ${{ matrix.platform.name == 'CMake' }}
         run: |
           cmake --build build --config Release --parallel --verbose
+      - name: Build test apk's (CMake)
+        if: ${{ matrix.platform.name == 'CMake' }}
+        run: |
+          cmake --build build --config Release --parallel --verbose --target testaudiocapture-apk testcontroller-apk testmultiaudio-apk testsprite-apk
       - name: Install (CMake)
         if: ${{ matrix.platform.name == 'CMake' }}
         run: |
@@ -88,3 +97,9 @@ jobs:
           if-no-files-found: error
           name: ${{ matrix.platform.artifact }}
           path: build/dist/SDL3*
+      - uses: actions/upload-artifact@v3
+        if: ${{ matrix.platform.name == 'CMake' }}
+        with:
+          if-no-files-found: error
+          name: ${{ matrix.platform.apk-artifact }}
+          path: build/test/*.apk

+ 67 - 2
CMakeLists.txt

@@ -368,9 +368,11 @@ endforeach()
 # Allow some projects to be built conditionally.
 set_option(SDL_DISABLE_INSTALL    "Disable installation of SDL3" ${SDL3_SUBPROJECT})
 cmake_dependent_option(SDL_DISABLE_INSTALL_CPACK "Create binary SDL3 archive using CPack" ${SDL3_SUBPROJECT} "NOT SDL_DISABLE_INSTALL" ON)
-cmake_dependent_option(SDL_DISABLE_INSTALL_MAN "Install man pages for SDL3" ${SDL3_SUBPROJECT} "NOT SDL_DISABLE_INSTALL;NOT SDL_FRAMEWORK" ON)
+cmake_dependent_option(SDL_DISABLE_INSTALL_DOCS "Install docs for SDL3" ${SDL3_SUBPROJECT} "NOT SDL_DISABLE_INSTALL;NOT SDL_FRAMEWORK" ON)
 set_option(SDL_DISABLE_UNINSTALL  "Disable uninstallation of SDL3" OFF)
 
+cmake_dependent_option(SDL_DISABLE_ANDROID_JAR  "Disable creation of SDL3.jar" ${SDL3_SUBPROJECT} "ANDROID" ON)
+
 option_string(SDL_ASSERTIONS "Enable internal sanity checks (auto/disabled/release/enabled/paranoid)" "auto")
 #set_option(SDL_DEPENDENCY_TRACKING "Use gcc -MMD -MT dependency tracking" ON)
 set_option(SDL_ASSEMBLY            "Enable assembly routines" ${SDL_ASSEMBLY_DEFAULT})
@@ -1310,6 +1312,8 @@ endif()
 
 # Platform-specific options and settings
 if(ANDROID)
+  list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake/android")
+
   file(GLOB ANDROID_CORE_SOURCES ${SDL3_SOURCE_DIR}/src/core/android/*.c)
   list(APPEND SOURCE_FILES ${ANDROID_CORE_SOURCES} ${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c)
   set_property(SOURCE "${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c" APPEND_STRING PROPERTY COMPILE_FLAGS " -Wno-declaration-after-statement")
@@ -1442,6 +1446,50 @@ if(ANDROID)
     set(HAVE_CLOCK_GETTIME 1)
   endif()
 
+  if(NOT SDL_DISABLE_ANDROID_JAR)
+    find_package(Java)
+    find_package(SdlAndroidPlatform MODULE)
+
+    if(Java_FOUND AND SdlAndroidPlatform_FOUND)
+      include(UseJava)
+      set(path_android_jar "${SDL_ANDROID_PLATFORM_ROOT}/android.jar")
+      set(android_java_sources_root "${SDL3_SOURCE_DIR}/android-project/app/src/main/java")
+      file(GLOB SDL_JAVA_SOURCES "${android_java_sources_root}/org/libsdl/app/*.java")
+      set(CMAKE_JAVA_COMPILE_FLAGS "-encoding;utf-8")
+      add_jar(SDL3-jar
+        SOURCES ${SDL_JAVA_SOURCES}
+        INCLUDE_JARS "${path_android_jar}"
+        OUTPUT_NAME "SDL3"
+        VERSION "${SDL3_VERSION}"
+      )
+      set_property(TARGET SDL3-jar PROPERTY OUTPUT "${SDL3_BINARY_DIR}/SDL3-${SDL3_VERSION}.jar")
+      set(javasourcesjar "${SDL3_BINARY_DIR}/SDL3-${SDL3_VERSION}-sources.jar")
+      string(REGEX REPLACE "${android_java_sources_root}/" "" sdl_relative_java_sources "${SDL_JAVA_SOURCES}")
+      add_custom_command(
+        OUTPUT "${javasourcesjar}"
+        COMMAND ${Java_JAR_EXECUTABLE} cf "${javasourcesjar}" ${sdl_relative_java_sources}
+        WORKING_DIRECTORY "${android_java_sources_root}"
+        DEPENDS ${SDL_JAVA_SOURCES}
+      )
+      add_custom_target(SDL3-javasources ALL DEPENDS "${javasourcesjar}")
+      if(NOT SDL_DISABLE_INSTALL_DOCS)
+        set(javadocdir "${SDL3_BINARY_DIR}/docs/javadoc")
+        set(javadocjar "${SDL3_BINARY_DIR}/SDL3-${SDL3_VERSION}-javadoc.jar")
+        set(javadoc_index_html "${javadocdir}/index.html")
+        add_custom_command(
+          OUTPUT "${javadoc_index_html}"
+          COMMAND ${CMAKE_COMMAND} -E rm -rf "${javadocdir}"
+          COMMAND ${Java_JAVADOC_EXECUTABLE} -encoding utf8 -d "${javadocdir}"
+            -classpath "${path_android_jar}"
+            -author -use -version ${SDL_JAVA_SOURCES}
+          DEPENDS ${SDL_JAVA_SOURCES} "${path_android_jar}"
+        )
+        add_custom_target(SDL3-javadoc ALL DEPENDS "${javadoc_index_html}")
+        set_property(TARGET SDL3-javadoc PROPERTY OUTPUT_DIR "${javadocdir}")
+      endif()
+    endif()
+  endif()
+
 elseif(EMSCRIPTEN)
   # Hide noisy warnings that intend to aid mostly during initial stages of porting a new
   # project. Uncomment at will for verbose cross-compiling -I/../ path info.
@@ -3695,11 +3743,28 @@ if(NOT SDL_DISABLE_INSTALL)
     include(CPack)
   endif()
 
-  if(NOT SDL_DISABLE_INSTALL_MAN)
+  if(ANDROID)
+    set(SDL_INSTALL_JAVADIR "${CMAKE_INSTALL_DATAROOTDIR}/java" CACHE PATH "Path where to install java clases + java sources")
+    if(TARGET SDL3-jar)
+      install(FILES "${SDL3_BINARY_DIR}/SDL3.jar"  "${SDL3_BINARY_DIR}/SDL3-${SDL3_VERSION}.jar"
+        DESTINATION "${SDL_INSTALL_JAVADIR}/SDL3")
+    endif()
+    if(TARGET SDL3-javasources)
+      install(FILES  "${SDL3_BINARY_DIR}/SDL3-${SDL3_VERSION}-sources.jar"
+        DESTINATION "${SDL_INSTALL_JAVADIR}/SDL3")
+    endif()
+  endif()
+
+  if(NOT SDL_DISABLE_INSTALL_DOCS)
     SDL_generate_manpages(
       SYMBOL "SDL_Init"
       WIKIHEADERS_PL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/build-scripts/wikiheaders.pl"
     )
+    if(TARGET SDL3-javadoc)
+      set(SDL_INSTALL_JAVADOCDIR "${CMAKE_INSTALL_DATAROOTDIR}/javadoc" CACHE PATH "Path where to install SDL3 javadoc")
+      install(DIRECTORY "${SDL3_BINARY_DIR}/docs/javadoc/"
+        DESTINATION "${SDL_INSTALL_JAVADOCDIR}/SDL3")
+    endif()
   endif()
 endif()
 

+ 103 - 0
cmake/android/FindSdlAndroid.cmake

@@ -0,0 +1,103 @@
+#[=======================================================================[
+
+FindSdlAndroid
+----------------------
+
+Locate various executables that are essential to creating an Android APK archive.
+This find module uses the FindSdlAndroidBuildTools module to locate some Android utils.
+
+
+Imported targets
+^^^^^^^^^^^^^^^^
+
+This module defines the following :prop_tgt:`IMPORTED` target(s):
+
+`` SdlAndroid::aapt2 ``
+   Imported executable for the "android package tool" v2
+
+`` SdlAndroid::apksigner``
+   Imported executable for the APK signer tool
+
+`` SdlAndroid::d8 ``
+   Imported executable for the dex compiler
+
+`` SdlAndroid::zipalign ``
+   Imported executable for the zipalign util
+
+`` SdlAndroid::adb ``
+   Imported executable for the "android debug bridge" tool
+
+`` SdlAndroid::keytool ``
+   Imported executable for the keytool, a key and certificate management utility
+
+`` SdlAndroid::zip ``
+   Imported executable for the zip, for packaging and compressing files
+
+Result variables
+^^^^^^^^^^^^^^^^
+
+This module will set the following variables in your project:
+
+`` AAPT2_BIN ``
+   Path of aapt2
+
+`` APKSIGNER_BIN ``
+   Path of apksigner
+
+`` D8_BIN ``
+   Path of d8
+
+`` ZIPALIGN_BIN ``
+   Path of zipalign
+
+`` ADB_BIN ``
+   Path of adb
+
+`` KEYTOOL_BIN ``
+   Path of keytool
+
+`` ZIP_BIN ``
+   Path of zip
+
+#]=======================================================================]
+
+cmake_minimum_required(VERSION 3.7)
+
+if(NOT PROJECT_NAME MATCHES "^SDL.*")
+  message(WARNING "This module is internal to SDL and is currently not supported.")
+endif()
+
+find_package(SdlAndroidBuildTools MODULE)
+
+function(_sdl_android_find_create_imported_executable NAME)
+  string(TOUPPER "${NAME}" NAME_UPPER)
+  set(varname "${NAME_UPPER}_BIN")
+  find_program("${varname}" NAMES "${NAME}" PATHS ${SDL_ANDROID_BUILD_TOOLS_ROOT})
+  if(EXISTS "${${varname}}" AND NOT TARGET SdlAndroid::${NAME})
+    add_executable(SdlAndroid::${NAME} IMPORTED)
+    set_property(TARGET SdlAndroid::${NAME} PROPERTY IMPORTED_LOCATION "${${varname}}")
+  endif()
+endfunction()
+
+if(SdlAndroidBuildTools_FOUND)
+  _sdl_android_find_create_imported_executable(aapt2)
+  _sdl_android_find_create_imported_executable(apksigner)
+  _sdl_android_find_create_imported_executable(d8)
+  _sdl_android_find_create_imported_executable(zipalign)
+endif()
+
+_sdl_android_find_create_imported_executable(adb)
+_sdl_android_find_create_imported_executable(keytool)
+_sdl_android_find_create_imported_executable(zip)
+include(FindPackageHandleStandardArgs)
+
+find_package_handle_standard_args(SdlAndroid
+  VERSION_VAR
+  REQUIRED_VARS
+    AAPT2_BIN
+    APKSIGNER_BIN
+    D8_BIN
+    ZIPALIGN_BIN
+    KEYTOOL_BIN
+    ZIP_BIN
+)

+ 115 - 0
cmake/android/FindSdlAndroidBuildTools.cmake

@@ -0,0 +1,115 @@
+#[=======================================================================[
+
+FindSdlAndroidBuildTools
+----------------------
+
+Locate the Android build tools directory.
+
+
+Imported targets
+^^^^^^^^^^^^^^^^
+
+This find module defines the following :prop_tgt:`IMPORTED` target(s):
+
+<none>
+
+Result variables
+^^^^^^^^^^^^^^^^
+
+This module will set the following variables in your project:
+
+`` SdlAndroidBuildTools_FOUND
+   if false, no Android build tools have been found
+
+`` SDL_ANDROID_BUILD_TOOLS_ROOT
+   path of the Android build tools root directory if found
+
+`` SDL_ANDROID_BUILD_TOOLS_VERSION
+   the human-readable string containing the android build tools version if found
+
+Cache variables
+^^^^^^^^^^^^^^^
+
+These variables may optionally be set to help this module find the correct files:
+
+``SDL_ANDROID_BUILD_TOOLS_ROOT``
+  path of the Android build tools root directory
+
+
+Variables for locating Android platform
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This module responds to the flags:
+
+``SDL_ANDROID_HOME
+    First, this module will look for platforms in this CMake variable.
+
+``ANDROID_HOME
+    If no platform was found in `SDL_ANDROID_HOME`, then try `ANDROID_HOME`.
+
+``$ENV{ANDROID_HOME}
+    If no platform was found in neither `SDL_ANDROID_HOME` or `ANDROID_HOME`, then try `ANDROID_HOME}`
+
+#]=======================================================================]
+
+cmake_minimum_required(VERSION 3.7)
+
+if(NOT PROJECT_NAME MATCHES "^SDL.*")
+  message(WARNING "This module is internal to SDL and is currently not supported.")
+endif()
+
+function(_sdl_is_valid_android_build_tools_root RESULT VERSION BUILD_TOOLS_ROOT)
+  set(result TRUE)
+  set(version -1)
+
+  string(REGEX MATCH "/([0-9.]+)$" root_match "${BUILD_TOOLS_ROOT}")
+  if(root_match
+      AND EXISTS "${BUILD_TOOLS_ROOT}/aapt2"
+      AND EXISTS "${BUILD_TOOLS_ROOT}/apksigner"
+      AND EXISTS "${BUILD_TOOLS_ROOT}/d8"
+      AND EXISTS "${BUILD_TOOLS_ROOT}/zipalign")
+    set(result "${BUILD_TOOLS_ROOT}")
+    set(version "${CMAKE_MATCH_1}")
+  endif()
+
+  set(${RESULT} ${result} PARENT_SCOPE)
+  set(${VERSION} ${version} PARENT_SCOPE)
+endfunction()
+
+function(_find_sdl_android_build_tools_root ROOT)
+  cmake_parse_arguments(fsabtr "" "" "" ${ARGN})
+  set(homes ${SDL_ANDROID_HOME} ${ANDROID_HOME} $ENV{ANDROID_HOME})
+  set(root ${ROOT}-NOTFOUND)
+  foreach(home IN LISTS homes)
+    if(NOT IS_DIRECTORY "${home}")
+      continue()
+    endif()
+    file(GLOB build_tools_roots LIST_DIRECTORIES true "${home}/build-tools/*")
+    set(max_build_tools_version -1)
+    set(max_build_tools_root "")
+    foreach(build_tools_root IN LISTS build_tools_roots)
+      _sdl_is_valid_android_build_tools_root(is_valid build_tools_version "${build_tools_root}")
+      if(is_valid AND build_tools_version GREATER max_build_tools_version)
+        set(max_build_tools_version "${build_tools_version}")
+        set(max_build_tools_root "${build_tools_root}")
+      endif()
+    endforeach()
+    if(max_build_tools_version GREATER -1)
+      set(root ${max_build_tools_root})
+      break()
+    endif()
+  endforeach()
+  set(${ROOT} ${root} PARENT_SCOPE)
+endfunction()
+
+if(NOT DEFINED SDL_ANDROID_BUILD_TOOLS_ROOT)
+  _find_sdl_android_build_tools_root(SDL_ANDROID_BUILD_TOOLS_ROOT)
+  set(SDL_ANDROID_BUILD_TOOLS_ROOT "${SDL_ANDROID_BUILD_TOOLS_ROOT}" CACHE PATH "Path of Android build tools")
+endif()
+
+include(FindPackageHandleStandardArgs)
+
+find_package_handle_standard_args(SdlAndroidBuildTools
+  VERSION_VAR SDL_ANDROID_BUILD_TOOLS_VERSION
+  REQUIRED_VARS SDL_ANDROID_BUILD_TOOLS_ROOT
+)

+ 123 - 0
cmake/android/FindSdlAndroidPlatform.cmake

@@ -0,0 +1,123 @@
+#[=======================================================================[
+
+FindSdlAndroidPlatform
+----------------------
+
+Locate the Android SDK platform.
+
+
+Imported targets
+^^^^^^^^^^^^^^^^
+
+This module defines the following :prop_tgt:`IMPORTED` target(s):
+
+<none>
+
+Result variables
+^^^^^^^^^^^^^^^^
+
+This find module will set the following variables in your project:
+
+`` SdlAndroidPlatform_FOUND
+   if false, no Android platform has been found
+
+`` SDL_ANDROID_PLATFORM_ROOT
+   path of the Android SDK platform root directory if found
+
+`` SDL_ANDROID_PLATFORM_ANDROID_JAR
+   path of the Android SDK platform jar file if found
+
+`` SDL_ANDROID_PLATFORM_VERSION
+   the human-readable string containing the android platform version if found
+
+Cache variables
+^^^^^^^^^^^^^^^
+
+These variables may optionally be set to help this module find the correct files:
+
+``SDL_ANDROID_PLATFORM_ROOT``
+  path of the Android SDK platform root directory
+
+
+Variables for locating Android platform
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This module responds to the flags:
+
+``SDL_ANDROID_HOME
+    First, this module will look for platforms in this CMake variable.
+
+``ANDROID_HOME
+    If no platform was found in `SDL_ANDROID_HOME`, then try `ANDROID_HOME`.
+
+``$ENV{ANDROID_HOME}
+    If no platform was found in neither `SDL_ANDROID_HOME` or `ANDROID_HOME`, then try `ANDROID_HOME}`
+
+#]=======================================================================]
+
+cmake_minimum_required(VERSION 3.7)
+
+if(NOT PROJECT_NAME MATCHES "^SDL.*")
+    message(WARNING "This module is internal to SDL and is currently not supported.")
+endif()
+
+function(_sdl_is_valid_android_platform_root RESULT VERSION PLATFORM_ROOT)
+    set(result FALSE)
+    set(version -1)
+
+    string(REGEX MATCH "/android-([0-9]+)$" root_match "${PLATFORM_ROOT}")
+    if(root_match AND EXISTS "${PLATFORM_ROOT}/android.jar")
+        set(result TRUE)
+        set(version "${CMAKE_MATCH_1}")
+    endif()
+
+    set(${RESULT} ${result} PARENT_SCOPE)
+    set(${VERSION} ${version} PARENT_SCOPE)
+endfunction()
+
+function(_sdl_find_android_platform_root ROOT)
+  cmake_parse_arguments(sfapr "" "" "" ${ARGN})
+  set(homes ${SDL_ANDROID_HOME} ${ANDROID_HOME} $ENV{ANDROID_HOME})
+  set(root ${ROOT}-NOTFOUND)
+  foreach(home IN LISTS homes)
+    if(NOT IS_DIRECTORY "${home}")
+      continue()
+    endif()
+    file(GLOB platform_roots LIST_DIRECTORIES true "${home}/platforms/*")
+    set(max_platform_version -1)
+    set(max_platform_root "")
+    foreach(platform_root IN LISTS platform_roots)
+      _sdl_is_valid_android_platform_root(is_valid platform_version "${platform_root}")
+      if(is_valid AND platform_version GREATER max_platform_version)
+        set(max_platform_version "${platform_version}")
+        set(max_platform_root "${platform_root}")
+      endif()
+    endforeach()
+    if(max_platform_version GREATER -1)
+      set(root ${max_platform_root})
+      break()
+    endif()
+  endforeach()
+  set(${ROOT} ${root} PARENT_SCOPE)
+endfunction()
+
+set(SDL_ANDROID_PLATFORM_ANDROID_JAR "SDL_ANDROID_PLATFORM_ANDROID_JAR-NOTFOUND")
+
+if(NOT DEFINED SDL_ANDROID_PLATFORM_ROOT)
+  _sdl_find_android_platform_root(SDL_ANDROID_PLATFORM_ROOT)
+  set(SDL_ANDROID_PLATFORM_ROOT "${SDL_ANDROID_PLATFORM_ROOT}" CACHE PATH "Path of Android platform")
+endif()
+if(SDL_ANDROID_PLATFORM_ROOT)
+  _sdl_is_valid_android_platform_root(_valid SDL_ANDROID_PLATFORM_VERSION "${SDL_ANDROID_PLATFORM_ROOT}")
+  if(_valid)
+    set(SDL_ANDROID_PLATFORM_ANDROID_JAR "${SDL_ANDROID_PLATFORM_ROOT}/android.jar")
+  endif()
+  unset(_valid)
+endif()
+
+include(FindPackageHandleStandardArgs)
+
+find_package_handle_standard_args(SdlAndroidPlatform
+  VERSION_VAR SDL_ANDROID_PLATFORM_VERSION
+  REQUIRED_VARS SDL_ANDROID_PLATFORM_ROOT SDL_ANDROID_PLATFORM_ANDROID_JAR
+)

+ 276 - 0
cmake/android/SdlAndroidFunctions.cmake

@@ -0,0 +1,276 @@
+#[=======================================================================[
+
+This CMake script contains functions to build an Android APK.
+It is (currently) limited to packaging binaries for a single architecture.
+
+#]=======================================================================]
+
+cmake_minimum_required(VERSION 3.7)
+
+if(NOT PROJECT_NAME MATCHES "^SDL.*")
+  message(WARNING "This module is internal to SDL and is currently not supported.")
+endif()
+
+function(_sdl_create_outdir_for_target OUTDIRECTORY TARGET)
+  set(outdir "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${TARGET}.dir")
+  # Some CMake versions have a slow `cmake -E make_directory` implementation
+  if(NOT IS_DIRECTORY "${outdir}")
+    execute_process(COMMAND "${CMAKE_COMMAND}" -E make_directory "${outdir}")
+  endif()
+  set("${OUTDIRECTORY}" "${outdir}" PARENT_SCOPE)
+endfunction()
+
+function(sdl_create_android_debug_keystore TARGET)
+  set(output "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_debug.keystore")
+  add_custom_command(OUTPUT ${output}
+    COMMAND ${CMAKE_COMMAND} -E rm -f "${output}"
+    COMMAND SdlAndroid::keytool -genkey -keystore "${output}" -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "C=US, O=Android, CN=Android Debug"
+  )
+  add_custom_target(${TARGET} DEPENDS "${output}")
+  set_property(TARGET ${TARGET} PROPERTY OUTPUT "${output}")
+endfunction()
+
+function(sdl_android_compile_resources TARGET)
+  cmake_parse_arguments(arg "" "RESFOLDER" "RESOURCES" ${ARGN})
+
+  if(NOT arg_RESFOLDER AND NOT arg_RESOURCES)
+    message(FATAL_ERROR "Missing RESFOLDER or RESOURCES argument (need one or both)")
+  endif()
+  _sdl_create_outdir_for_target(outdir "${TARGET}")
+  set(out_files "")
+
+  set(res_files "")
+  if(arg_RESFOLDER)
+    get_filename_component(arg_RESFOLDER "${arg_RESFOLDER}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+    file(GLOB_RECURSE res_folder_files "${arg_RESFOLDER}/*")
+    list(APPEND res_files ${res_folder_files})
+
+    foreach(res_file IN LISTS res_files)
+      file(RELATIVE_PATH rel_res_file "${arg_RESFOLDER}" "${res_file}")
+      string(REPLACE "/" "_" rel_comp_path "${rel_res_file}")
+      if(res_file MATCHES ".*res/values.*\\.xml$")
+        string(REGEX REPLACE "\\.xml" ".arsc" rel_comp_path "${rel_comp_path}")
+      endif()
+      set(comp_path "${outdir}/${rel_comp_path}.flat")
+      add_custom_command(
+        OUTPUT "${comp_path}"
+        COMMAND SdlAndroid::aapt2 compile -o "${outdir}" "${res_file}"
+        DEPENDS ${res_file}
+      )
+      list(APPEND out_files "${comp_path}")
+    endforeach()
+  endif()
+
+  if(arg_RESOURCES)
+    list(APPEND res_files ${arg_RESOURCES})
+    foreach(res_file IN LISTS arg_RESOURCES)
+      string(REGEX REPLACE ".*/res/" "" rel_res_file ${res_file})
+      string(REPLACE "/" "_" rel_comp_path "${rel_res_file}")
+      if(res_file MATCHES ".*res/values.*\\.xml$")
+        string(REGEX REPLACE "\\.xml" ".arsc" rel_comp_path "${rel_comp_path}")
+      endif()
+      set(comp_path "${outdir}/${rel_comp_path}.flat")
+      add_custom_command(
+        OUTPUT "${comp_path}"
+        COMMAND SdlAndroid::aapt2 compile -o "${outdir}" "${res_file}"
+        DEPENDS ${res_file}
+      )
+      list(APPEND out_files "${comp_path}")
+    endforeach()
+  endif()
+
+  add_custom_target(${TARGET} DEPENDS ${out_files})
+  set_property(TARGET "${TARGET}" PROPERTY OUTPUTS "${out_files}")
+  set_property(TARGET "${TARGET}" PROPERTY SOURCES "${res_files}")
+endfunction()
+
+function(sdl_android_link_resources TARGET)
+  cmake_parse_arguments(arg "NO_DEBUG" "MIN_SDK_VERSION;TARGET_SDK_VERSION;ANDROID_JAR;OUTPUT_APK;MANIFEST;PACKAGE" "RES_TARGETS" ${ARGN})
+
+  if(arg_MANIFEST)
+    get_filename_component(arg_MANIFEST "${arg_MANIFEST}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+  else()
+    message(FATAL_ERROR "sdl_add_android_link_resources_target requires a Android MANIFEST path (${arg_MANIFEST})")
+  endif()
+  if(NOT arg_PACKAGE)
+    file(READ "${arg_MANIFEST}" manifest_contents)
+    string(REGEX MATCH "package=\"([a-zA-Z0-9_.]+)\"" package_match "${manifest_contents}")
+    if(NOT package_match)
+      message(FATAL_ERROR "Could not extract package from Android manifest (${arg_MANIFEST})")
+    endif()
+    set(arg_PACKAGE "${CMAKE_MATCH_1}")
+  endif()
+
+  set(depends "")
+
+  _sdl_create_outdir_for_target(outdir "${TARGET}")
+  string(REPLACE "." "/" java_r_path "${arg_PACKAGE}")
+  get_filename_component(java_r_path "${java_r_path}" ABSOLUTE BASE_DIR "${outdir}")
+  set(java_r_path "${java_r_path}/R.java")
+
+  set(command SdlAndroid::aapt2 link)
+  if(NOT arg_NO_DEBUG)
+    list(APPEND command --debug-mode)
+  endif()
+  if(arg_MIN_SDK_VERSION)
+    list(APPEND command --min-sdk-version ${arg_MIN_SDK_VERSION})
+  endif()
+  if(arg_TARGET_SDK_VERSION)
+    list(APPEND command --target-sdk-version ${arg_TARGET_SDK_VERSION})
+  endif()
+  if(arg_ANDROID_JAR)
+    list(APPEND command -I "${arg_ANDROID_JAR}")
+  else()
+    list(APPEND command -I "${SDL_ANDROID_PLATFORM_ANDROID_JAR}")
+  endif()
+  if(NOT arg_OUTPUT_APK)
+    set(arg_OUTPUT_APK "${TARGET}.apk")
+  endif()
+  get_filename_component(arg_OUTPUT_APK "${arg_OUTPUT_APK}" ABSOLUTE BASE_DIR "${outdir}")
+  list(APPEND command -o "${arg_OUTPUT_APK}")
+  list(APPEND command --java "${outdir}")
+  list(APPEND command --manifest "${arg_MANIFEST}")
+  foreach(res_target IN LISTS arg_RES_TARGETS)
+    list(APPEND command $<TARGET_PROPERTY:${res_target},OUTPUTS>)
+    list(APPEND depends $<TARGET_PROPERTY:${res_target},OUTPUTS>)
+  endforeach()
+  add_custom_command(
+    OUTPUT "${arg_OUTPUT_APK}" "${java_r_path}"
+    COMMAND ${command}
+    DEPENDS ${depends} ${arg_MANIFEST}
+    COMMAND_EXPAND_LISTS
+    VERBATIM
+  )
+  add_custom_target(${TARGET} DEPENDS "${arg_OUTPUT_APK}" "${java_r_path}")
+  set_property(TARGET ${TARGET} PROPERTY OUTPUT "${arg_OUTPUT_APK}")
+  set_property(TARGET ${TARGET} PROPERTY JAVA_R "${java_r_path}")
+  set_property(TARGET ${TARGET} PROPERTY OUTPUTS "${${arg_OUTPUT_APK}};${java_r_path}")
+endfunction()
+
+function(sdl_add_to_apk_unaligned TARGET)
+  cmake_parse_arguments(arg "" "APK_IN;NAME;OUTDIR" "ASSETS;NATIVE_LIBS;DEX" ${ARGN})
+
+  if(NOT arg_APK_IN)
+    message(FATAL_ERROR "Missing APK_IN argument")
+  endif()
+
+  if(NOT TARGET ${arg_APK_IN})
+    message(FATAL_ERROR "APK_IN (${arg_APK_IN}) must be a target providing an apk")
+  endif()
+
+  _sdl_create_outdir_for_target(workdir ${TARGET})
+
+  if(NOT arg_OUTDIR)
+    set(arg_OUTDIR "${CMAKE_CURRENT_BINARY_DIR}")
+  endif()
+
+  if(NOT arg_NAME)
+    string(REGEX REPLACE "[:-]+" "." arg_NAME "${TARGET}")
+    if(NOT arg_NAME MATCHES "\\.apk")
+      set(arg_NAME "${arg_NAME}.apk")
+    endif()
+  endif()
+  get_filename_component(apk_file "${arg_NAME}" ABSOLUTE BASE_DIR "${arg_OUTDIR}")
+
+  set(apk_libdir "lib/${ANDROID_ABI}")
+
+  set(depends "")
+
+  set(commands
+    COMMAND "${CMAKE_COMMAND}" -E remove_directory -rf "${apk_libdir}" "assets"
+    COMMAND "${CMAKE_COMMAND}" -E make_directory "${apk_libdir}" "assets"
+    COMMAND "${CMAKE_COMMAND}" -E copy "$<TARGET_PROPERTY:${arg_APK_IN},OUTPUT>" "${apk_file}"
+  )
+
+  set(dex_i "1")
+  foreach(dex IN LISTS arg_DEX)
+    set(suffix "${dex_i}")
+    if(suffix STREQUAL "1")
+      set(suffix "")
+    endif()
+    list(APPEND commands
+      COMMAND "${CMAKE_COMMAND}" -E copy "$<TARGET_PROPERTY:${dex},OUTPUT>" "classes${suffix}.dex"
+      COMMAND SdlAndroid::zip -u -q -j "${apk_file}" "classes${suffix}.dex"
+    )
+    math(EXPR dex_i "${dex_i}+1")
+    list(APPEND depends "$<TARGET_PROPERTY:${dex},OUTPUT>")
+  endforeach()
+
+  foreach(native_lib IN LISTS arg_NATIVE_LIBS)
+    list(APPEND commands
+      COMMAND "${CMAKE_COMMAND}" -E copy $<TARGET_FILE:${native_lib}> "${apk_libdir}/$<TARGET_FILE_NAME:${native_lib}>"
+      COMMAND SdlAndroid::zip -u -q "${apk_file}" "${apk_libdir}/$<TARGET_FILE_NAME:${native_lib}>"
+    )
+  endforeach()
+  if(arg_ASSETS)
+    list(APPEND commands
+      COMMAND "${CMAKE_COMMAND}" -E copy ${arg_ASSETS} "assets"
+      COMMAND SdlAndroid::zip -u -r -q "${apk_file}" "assets"
+    )
+  endif()
+
+  add_custom_command(OUTPUT "${apk_file}"
+    ${commands}
+    DEPENDS ${arg_NATIVE_LIBS} ${depends} "$<TARGET_PROPERTY:${arg_APK_IN},OUTPUT>"
+    WORKING_DIRECTORY "${workdir}"
+  )
+  add_custom_target(${TARGET} DEPENDS "${apk_file}")
+  set_property(TARGET ${TARGET} PROPERTY OUTPUT "${apk_file}")
+endfunction()
+
+function(sdl_apk_align TARGET APK_IN)
+  cmake_parse_arguments(arg "" "NAME;OUTDIR" "" ${ARGN})
+
+  if(NOT TARGET ${arg_APK_IN})
+    message(FATAL_ERROR "APK_IN (${arg_APK_IN}) must be a target providing an apk")
+  endif()
+
+  if(NOT arg_OUTDIR)
+    set(arg_OUTDIR "${CMAKE_CURRENT_BINARY_DIR}")
+  endif()
+
+  if(NOT arg_NAME)
+    string(REGEX REPLACE "[:-]+" "." arg_NAME "${TARGET}")
+    if(NOT arg_NAME MATCHES "\\.apk")
+      set(arg_NAME "${arg_NAME}.apk")
+    endif()
+  endif()
+  get_filename_component(apk_file "${arg_NAME}" ABSOLUTE BASE_DIR "${arg_OUTDIR}")
+
+  add_custom_command(OUTPUT "${apk_file}"
+    COMMAND SdlAndroid::zipalign -f 4 "$<TARGET_PROPERTY:${APK_IN},OUTPUT>" "${apk_file}"
+    DEPENDS "$<TARGET_PROPERTY:${APK_IN},OUTPUT>"
+  )
+  add_custom_target(${TARGET} DEPENDS "${apk_file}")
+  set_property(TARGET ${TARGET} PROPERTY OUTPUT "${apk_file}")
+endfunction()
+
+function(sdl_apk_sign TARGET APK_IN)
+  cmake_parse_arguments(arg "" "OUTPUT;KEYSTORE" "" ${ARGN})
+
+  if(NOT TARGET ${arg_APK_IN})
+    message(FATAL_ERROR "APK_IN (${arg_APK_IN}) must be a target providing an apk")
+  endif()
+
+  if(NOT TARGET ${arg_KEYSTORE})
+    message(FATAL_ERROR "APK_KEYSTORE (${APK_KEYSTORE}) must be a target providing a keystore")
+  endif()
+
+  if(NOT arg_OUTPUT)
+    string(REGEX REPLACE "[:-]+" "." arg_OUTPUT "${TARGET}")
+    if(NOT arg_OUTPUT MATCHES "\\.apk")
+      set(arg_OUTPUT "${arg_OUTPUT}.apk")
+    endif()
+  endif()
+  get_filename_component(apk_file "${arg_OUTPUT}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}")
+
+  add_custom_command(OUTPUT "${apk_file}"
+    COMMAND SdlAndroid::apksigner sign
+      --ks "$<TARGET_PROPERTY:${arg_KEYSTORE},OUTPUT>"
+      --ks-pass pass:android --in "$<TARGET_PROPERTY:${APK_IN},OUTPUT>" --out "${apk_file}"
+    DEPENDS "$<TARGET_PROPERTY:${APK_IN},OUTPUT>" "$<TARGET_PROPERTY:${arg_KEYSTORE},OUTPUT>"
+    BYPRODUCTS "${apk_file}.idsig"
+  )
+  add_custom_target(${TARGET} DEPENDS "${apk_file}")
+  set_property(TARGET ${TARGET} PROPERTY OUTPUT "${apk_file}")
+endfunction()

+ 74 - 0
cmake/android/SdlAndroidScript.cmake

@@ -0,0 +1,74 @@
+#[=======================================================================[
+
+This CMake script is meant to be used in CMake script mode (cmake -P).
+It wraps commands that communicate with an actual Android device.
+Because
+
+#]=======================================================================]
+
+cmake_minimum_required(VERSION 3.16)
+
+if(NOT CMAKE_SCRIPT_MODE_FILE)
+  message(FATAL_ERROR "This file can only be used in CMake script mode")
+endif()
+if(NOT ADB)
+  set(ADB "adb")
+endif()
+
+if(NOT ACTION)
+  message(FATAL_ERROR "Missing ACTION argument")
+endif()
+
+if(ACTION STREQUAL "uninstall")
+  # The uninstall action attempts to uninstall all packages. All failures are ignored.
+  foreach(package IN LISTS PACKAGES)
+    message("Uninstalling ${package} ...")
+    execute_process(
+        COMMAND ${ADB} uninstall ${package}
+        RESULT_VARIABLE res
+    )
+    message("... result=${res}")
+  endforeach()
+elseif(ACTION STREQUAL "install")
+  # The install actions attempts to install APK's to an Android device using adb. Failures are ignored.
+  set(failed_apks "")
+  foreach(apk IN LISTS APKS)
+    message("Installing ${apk} ...")
+    execute_process(
+        COMMAND ${ADB} install -d -r --streaming ${apk}
+        RESULT_VARIABLE res
+    )
+    message("... result=${res}")
+    if(NOT res EQUAL 0)
+      list(APPEND failed_apks ${apk})
+    endif()
+  endforeach()
+  if(failed_apks)
+    message(FATAL_ERROR "Failed to install ${failed_apks}")
+  endif()
+elseif(ACTION STREQUAL "build-install-run")
+  if(NOT EXECUTABLES)
+    message(FATAL_ERROR "Missing EXECUTABLES (don't know what executables to build/install and start")
+  endif()
+  if(NOT BUILD_FOLDER)
+    message(FATAL_ERROR "Missing BUILD_FOLDER (don't know where to build the APK's")
+  endif()
+  set(install_targets "")
+  foreach(executable IN LISTS EXECUTABLES)
+    list(APPEND install_targets "install-${executable}")
+  endforeach()
+  execute_process(
+      COMMAND ${CMAKE_COMMAND} --build "${BUILD_FOLDER}" --target ${install_targets}
+      RESULT_VARIABLE res
+  )
+  if(NOT res EQUAL 0)
+    message(FATAL_ERROR "Failed to install APK(s) for ${EXECUTABLES}")
+  endif()
+  list(GET EXECUTABLES 0 start_executable)
+  execute_process(
+      COMMAND ${CMAKE_COMMAND} --build "${BUILD_FOLDER}" --target start-${start_executable}
+      RESULT_VARIABLE res
+  )
+else()
+  message(FATAL_ERROR "Unknown ACTION=${ACTION}")
+endif()

+ 76 - 0
docs/README-android.md

@@ -485,3 +485,79 @@ Known issues
 - The number of buttons reported for each joystick is hardcoded to be 36, which
 is the current maximum number of buttons Android can report.
 
+Building the SDL tests
+================================================================================
+
+SDL's CMake build system can create APK's for the tests.
+It can build all tests with a single command without a dependency on gradle or Android Studio.
+The APK's are signed with a debug certificate.
+The only caveat is that the APK's support a single architecture. 
+
+### Requirements
+- SDL source tree
+- CMake
+- ninja or make
+- Android Platform SDK
+- Android NDK
+- Android Build tools
+- Java JDK (version should be compatible with Android)
+- keytool (usually provided with the Java JDK), used for generating a debug certificate
+- zip
+
+### CMake configuration
+
+When configuring the CMake project, you need to use the Android NDK CMake toolchain, and pass the Android home path through `SDL_ANDROID_HOME`.
+```
+cmake .. -DCMAKE_TOOLCHAIN_FILE=<path/to/android.toolchain.cmake> -DANDROID_ABI=<android-abi> -DSDL_ANDROID_HOME=<path-to-android-sdk-home> -DANDROID_PLATFORM=23 -DSDL_TESTS=ON
+```
+
+Remarks:
+- `android.toolchain.cmake` can usually be found at `$ANDROID_HOME/ndk/x.y.z/build/cmake/android.toolchain.cmake`
+- `ANDROID_ABI` should be one of `arm64-v8a`, `armeabi-v7a`, `x86` or `x86_64`.
+- When CMake is unable to find required paths, use `cmake-gui` to override required `SDL_ANDROID_` CMake cache variables.
+
+### Building the APK's
+
+For the `testsprite` executable, the `testsprite-apk` target will build the associated APK:
+```
+cmake --build . --target testsprite-apk
+```
+
+APK's of all tests can be built with the `sdl-test-apks` target:
+```
+cmake --build . --target sdl-test-apks
+```
+
+### Installation/removal of the tests
+
+`testsprite.apk` APK can be installed on your Android machine using the `install-testsprite` target:
+```
+cmake --build . --target install-testsprite
+```
+
+APK's of all tests can be installed with the `install-sdl-test-apks` target:
+```
+cmake --build . --target install-sdl-test-apks
+```
+
+All SDL tests can be uninstalled with the `uninstall-sdl-test-apks` target:
+```
+cmake --build . --target uninstall-sdl-test-apks
+```
+
+### Starting the tests
+
+After installation, the tests can be started using the Android Launcher GUI.
+Alternatively, they can also be started using CMake targets.
+
+This command will start the testsprite executable:
+```
+cmake --build . --target start-testsprite
+```
+
+There is also a convenience target which will build, install and start a test:
+```
+cmake --build . --target build-install-start-testsprite
+```
+
+Not all tests provide a GUI. For those, you can use `adb logcat` to read the output of stdout.  

+ 135 - 0
test/CMakeLists.txt

@@ -460,3 +460,138 @@ if(SDL_INSTALL_TESTS)
         DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/installed-tests/SDL3
     )
 endif()
+
+if(ANDROID AND TARGET SDL3-jar)
+    list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../cmake/android")
+    find_package(SdlAndroid MODULE)
+    if(SdlAndroid_FOUND)
+        set(apks "")
+        set(packages "")
+
+        include(SdlAndroidFunctions)
+        sdl_create_android_debug_keystore(SDL_test-debug-keystore)
+        sdl_android_compile_resources(SDL_test-resources RESFOLDER android/res)
+        add_custom_target(sdl-test-apks)
+        foreach(TEST ${SDL_TEST_EXECUTABLES})
+            set(ANDROID_MANIFEST_APP_NAME "${TEST}")
+            set(ANDROID_MANIFEST_LABEL "${TEST}")
+            set(ANDROID_MANIFEST_LIB_NAME "$<TARGET_FILE_BASE_NAME:${TEST}>")
+            set(ANDROID_MANIFEST_PACKAGE "org.libsdl.sdl.test.${TEST}")
+            set(generated_manifest_path "${CMAKE_CURRENT_BINARY_DIR}/android/${TEST}-src/AndroidManifest.xml")
+            string(REPLACE "." "/" JAVA_PACKAGE_DIR "${ANDROID_MANIFEST_PACKAGE}")
+            set(GENERATED_SRC_FOLDER "${CMAKE_CURRENT_BINARY_DIR}/android/${TEST}-src")
+            set(GENERATED_RES_FOLDER "${GENERATED_SRC_FOLDER}/res")
+            set(JAVA_PACKAGE_DIR "${GENERATED_SRC_FOLDER}/${JAVA_PACKAGE_DIR}")
+            configure_file(android/cmake/SDLEntryTestActivity.java.cmake "${JAVA_PACKAGE_DIR}/SDLEntryTestActivity.java" @ONLY)
+            configure_file(android/cmake/SDLTestActivity.java.cmake "${JAVA_PACKAGE_DIR}/SDLTestActivity.java" @ONLY)
+            configure_file(android/cmake/res/values/strings.xml.cmake android/res/values/strings-${TEST}.xml @ONLY)
+            configure_file(android/cmake/res/xml/shortcuts.xml.cmake "${GENERATED_RES_FOLDER}/xml/shortcuts.xml" @ONLY)
+            configure_file(android/cmake/AndroidManifest.xml.cmake "${generated_manifest_path}" @ONLY)
+            file(GENERATE
+                OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/android/${TEST}-$<CONFIG>/res/values/strings.xml"
+                INPUT "${CMAKE_CURRENT_BINARY_DIR}/android/res/values/strings-${TEST}.xml"
+            )
+
+            sdl_android_compile_resources(${TEST}-resources
+                RESOURCES
+                    "${CMAKE_CURRENT_BINARY_DIR}/android/${TEST}-$<CONFIG>/res/values/strings.xml"
+                    "${GENERATED_RES_FOLDER}/xml/shortcuts.xml"
+            )
+
+            sdl_android_link_resources(${TEST}-apk-linked
+                MANIFEST "${generated_manifest_path}"
+                PACKAGE ${ANDROID_MANIFEST_PACKAGE}
+                RES_TARGETS SDL_test-resources ${TEST}-resources
+                TARGET_SDK_VERSION 31
+            )
+
+            set(CMAKE_JAVA_COMPILE_FLAGS "-encoding;utf-8")
+            set(classes_path "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${TEST}-java.dir/classes")
+            # Some CMake versions have a slow `cmake -E make_directory` implementation
+            if(NOT IS_DIRECTORY "${classes_path}")
+                execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory "${classes_path}")
+            endif()
+            set(OUT_JAR "${CMAKE_CURRENT_BINARY_DIR}/${TEST}.jar")
+            add_custom_command(
+                OUTPUT "${OUT_JAR}"
+                COMMAND ${CMAKE_COMMAND} -E rm -rf "${classes_path}"
+                COMMAND ${CMAKE_COMMAND} -E make_directory "${classes_path}"
+                COMMAND ${Java_JAVAC_EXECUTABLE}
+                    -source 1.8 -target 1.8
+                    -bootclasspath "$<TARGET_PROPERTY:SDL3-jar,OUTPUT>"
+                    "${JAVA_PACKAGE_DIR}/SDLEntryTestActivity.java"
+                    "${JAVA_PACKAGE_DIR}/SDLTestActivity.java"
+                    $<TARGET_PROPERTY:${TEST}-apk-linked,JAVA_R>
+                    -cp "$<TARGET_PROPERTY:SDL3-jar,OUTPUT>:${path_android_jar}"
+                    -d "${classes_path}"
+                COMMAND ${Java_JAR_EXECUTABLE} cf "${OUT_JAR}" -C "${classes_path}" .
+                DEPENDS $<TARGET_PROPERTY:${TEST}-apk-linked,OUTPUTS> "$<TARGET_PROPERTY:SDL3-jar,OUTPUT>" "${JAVA_PACKAGE_DIR}/SDLTestActivity.java" "${JAVA_PACKAGE_DIR}/SDLEntryTestActivity.java"
+            )
+            add_custom_target(${TEST}-jar DEPENDS "${OUT_JAR}")
+            set_property(TARGET ${TEST}-jar PROPERTY OUTPUT "${OUT_JAR}")
+
+            set(dexworkdir "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${TEST}-dex.dir")
+            # Some CMake versions have a slow `cmake -E make_directory` implementation
+            if(NOT IS_DIRECTORY "${dexworkdir}")
+                execute_process(COMMAND "${CMAKE_COMMAND}" -E make_directory "${dexworkdir}")
+            endif()
+            set(classes_dex_base_name "classes.dex")
+            set(classes_dex "${dexworkdir}/${classes_dex_base_name}")
+            add_custom_command(
+                OUTPUT "${classes_dex}"
+                COMMAND SdlAndroid::d8
+                    $<TARGET_PROPERTY:${TEST}-jar,OUTPUT>
+                    $<TARGET_PROPERTY:SDL3-jar,OUTPUT>
+                    --lib "${path_android_jar}"
+                    --output "${dexworkdir}"
+                DEPENDS $<TARGET_PROPERTY:${TEST}-jar,OUTPUT> $<TARGET_PROPERTY:SDL3-jar,OUTPUT>
+            )
+            add_custom_target(${TEST}-dex DEPENDS "${classes_dex}")
+            set_property(TARGET ${TEST}-dex PROPERTY OUTPUT "${classes_dex}")
+            set_property(TARGET ${TEST}-dex PROPERTY OUTPUT_BASE_NAME "${classes_dex_base_name}")
+
+            sdl_add_to_apk_unaligned(${TEST}-unaligned-apk
+                APK_IN ${TEST}-apk-linked
+                OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/intermediates"
+                ASSETS ${RESOURCE_FILES}
+                NATIVE_LIBS SDL3::SDL3-shared ${TEST}
+                DEX ${TEST}-dex
+            )
+
+            sdl_apk_align(${TEST}-aligned-apk ${TEST}-unaligned-apk
+                OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/intermediates"
+            )
+            sdl_apk_sign(${TEST}-apk ${TEST}-aligned-apk
+                KEYSTORE SDL_test-debug-keystore
+            )
+            add_dependencies(sdl-test-apks ${TEST}-apk)
+
+            if(TARGET SdlAndroid::adb)
+                add_custom_target(install-${TEST}
+                    COMMAND "${CMAKE_COMMAND}" -DACTION=install "-DAPKS=$<TARGET_PROPERTY:${TEST}-apk,OUTPUT>" -P "${SDL3_SOURCE_DIR}/cmake/android/SdlAndroidScript.cmake"
+                    DEPENDS "${TEST}-apk"
+                )
+                add_custom_target(start-${TEST}
+                    COMMAND "${ADB_BIN}" shell am start-activity -S "${ANDROID_MANIFEST_PACKAGE}/.SDLTestActivity"
+                )
+                add_custom_target(build-install-start-${TEST}
+                    COMMAND "${CMAKE_COMMAND}" -DACTION=build-install-run "-DEXECUTABLES=${TEST}" "-DBUILD_FOLDER=${CMAKE_BINARY_DIR}" -P "${SDL3_SOURCE_DIR}/cmake/android/SdlAndroidScript.cmake"
+                )
+            endif()
+
+            list(APPEND packages "${ANDROID_MANIFEST_PACKAGE}")
+            list(APPEND install_targets install-${TEST})
+        endforeach()
+
+        if(TARGET SdlAndroid::adb)
+            add_custom_target(install-sdl-test-apks
+                DEPENDS ${install_targets}
+                VERBATIM
+            )
+            add_custom_target(uninstall-sdl-test-apks
+                COMMAND "${CMAKE_COMMAND}" "-DADB=$<TARGET_FILE:SdlAndroid::adb>" -DACTION=uninstall "-DPACKAGES=${packages}" -P "${SDL3_SOURCE_DIR}/cmake/android/SdlAndroidScript.cmake"
+                VERBATIM
+            )
+        endif()
+    endif()
+endif()

+ 70 - 0
test/android/cmake/AndroidManifest.xml.cmake

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="@ANDROID_MANIFEST_PACKAGE@">
+
+    <!-- OpenGL ES 2.0 -->
+    <uses-feature android:glEsVersion="0x00020000" />
+
+    <!-- Touchscreen support -->
+    <uses-feature
+        android:name="android.hardware.touchscreen"
+        android:required="false" />
+
+    <!-- Game controller support -->
+    <uses-feature
+        android:name="android.hardware.bluetooth"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.gamepad"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.usb.host"
+        android:required="false" />
+
+    <!-- External mouse input events -->
+    <uses-feature
+        android:name="android.hardware.type.pc"
+        android:required="false" />
+
+    <!-- Allow access to the vibrator -->
+    <uses-permission android:name="android.permission.VIBRATE" />
+
+    <!-- Allow access to the microphone -->
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/sdl-test"
+        android:roundIcon="@mipmap/sdl-test_round"
+        android:label="@string/label"
+        android:supportsRtl="true"
+        android:hardwareAccelerated="true"
+        tools:targetApi="31">
+        <activity
+            android:name="@[email protected]"
+            android:exported="true"
+            android:label="@string/label"
+            android:alwaysRetainTaskState="true"
+            android:launchMode="singleInstance"
+            android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
+            android:preferMinimalPostProcessing="true"
+            android:screenOrientation="fullSensor">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
+            </intent-filter>
+            <meta-data
+                android:name="android.app.shortcuts"
+                android:resource="@xml/shortcuts" />
+        </activity>
+        <activity
+            android:name="@[email protected]"
+            android:exported="false"
+            android:label="@string/label">
+        </activity>
+    </application>
+</manifest>

+ 121 - 0
test/android/cmake/SDLEntryTestActivity.java.cmake

@@ -0,0 +1,121 @@
+package @ANDROID_MANIFEST_PACKAGE@;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import org.libsdl.app.SDL;
+import org.libsdl.app.SDLActivity;
+
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+
+public class SDLEntryTestActivity extends Activity {
+
+    public String MODIFY_ARGUMENTS = "@[email protected]_ARGUMENTS";
+    boolean isModifyingArguments;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        Log.v("SDL", "SDLEntryTestActivity onCreate");
+        super.onCreate(savedInstanceState);
+
+        String intent_action = getIntent().getAction();
+        Log.v("SDL", "SDLEntryTestActivity intent.action = " + intent_action);
+
+        if (intent_action == MODIFY_ARGUMENTS) {
+            isModifyingArguments = true;
+            createArgumentLayout();
+        } else {
+            startChildActivityAndFinish();
+        }
+    }
+
+    protected void createArgumentLayout() {
+        LayoutInflater inflater = getLayoutInflater();
+        View view = inflater.inflate(R.layout.arguments_layout, null);
+        setContentView(view);
+
+        Button button = (Button)requireViewById(R.id.arguments_start_button);
+        button.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                startChildActivityAndFinish();
+            }
+        });
+    }
+
+    protected String[] getArguments() {
+        if (!isModifyingArguments) {
+            return new String[0];
+        }
+        EditText editText = (EditText)findViewById(R.id.arguments_edit);
+        String text = editText.getText().toString();
+        String new_text = text.replace("[ \t]*[ \t\n]+[ \t]+", "\n").strip();
+        Log.v("SDL", "text = " + text + "\n becomes \n" + new_text);
+        return new_text.split("\n", 0);
+    }
+
+    @Override
+    protected void onStart() {
+        Log.v("SDL", "SDLEntryTestActivity onStart");
+        super.onStart();
+    }
+
+    @Override
+    protected void onResume() {
+        Log.v("SDL", "SDLEntryTestActivity onResume");
+        super.onResume();
+    }
+
+    @Override
+    protected void onPause() {
+        Log.v("SDL", "SDLEntryTestActivity onPause");
+        super.onPause();
+    }
+
+    @Override
+    protected void onStop() {
+        Log.v("SDL", "SDLEntryTestActivity onStop");
+        super.onStop();
+    }
+
+    @Override
+    protected void onDestroy() {
+        Log.v("SDL", "SDLEntryTestActivity onDestroy");
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Bundle savedInstanceState) {
+        Log.v("SDL", "SDLEntryTestActivity onRestoreInstanceState");
+        super.onRestoreInstanceState(savedInstanceState);
+        EditText editText = (EditText)findViewById(R.id.arguments_edit);
+        editText.setText(savedInstanceState.getCharSequence("args", ""), TextView.BufferType.EDITABLE);
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        Log.v("SDL", "SDLEntryTestActivity onSaveInstanceState");
+        EditText editText = (EditText)findViewById(R.id.arguments_edit);
+        outState.putCharSequence("args", editText.getText());
+        super.onSaveInstanceState(outState);
+    }
+
+    private void startChildActivityAndFinish() {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        intent.setClassName("@ANDROID_MANIFEST_PACKAGE@", "@[email protected]");
+        intent.putExtra("arguments", getArguments());
+        startActivity(intent);
+        finish();
+    }
+}

+ 33 - 0
test/android/cmake/SDLTestActivity.java.cmake

@@ -0,0 +1,33 @@
+package @ANDROID_MANIFEST_PACKAGE@;
+
+import org.libsdl.app.SDLActivity;
+
+import android.os.Bundle;
+import android.util.Log;
+
+public class SDLTestActivity extends SDLActivity {
+    private String[] m_arguments;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        m_arguments = getIntent().getStringArrayExtra("arguments");
+        if (m_arguments == null) {
+            m_arguments = new String[0];
+        }
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    protected String[] getLibraries() {
+        return new String[] { getString(R.string.lib_name) };
+    }
+
+    @Override
+    protected String[] getArguments() {
+        Log.v("SDLTest", "#arguments = " + m_arguments.length);
+        for(int i = 0; i < m_arguments.length; i++) {
+            Log.v("SDLTest", "argument[" + i + "] = " + m_arguments[i]);
+        }
+        return m_arguments;
+    }
+}

+ 5 - 0
test/android/cmake/res/values/strings.xml.cmake

@@ -0,0 +1,5 @@
+<resources>
+    <string name="app_name">@ANDROID_MANIFEST_APP_NAME@</string>
+    <string name="lib_name">@ANDROID_MANIFEST_LIB_NAME@</string>
+    <string name="label">@ANDROID_MANIFEST_LABEL@</string>
+</resources>

+ 24 - 0
test/android/cmake/res/xml/shortcuts.xml.cmake

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+    <shortcut
+        android:shortcutId="modifyArguments"
+        android:enabled="true"
+        android:icon="@drawable/sdl-test_foreground"
+        android:shortcutShortLabel="@string/shortcutModifyArgumentsShortLabel">
+        <intent
+            android:action="@[email protected]_ARGUMENTS"
+            android:targetPackage="@ANDROID_MANIFEST_PACKAGE@"
+            android:targetClass="@[email protected]" />
+    </shortcut>
+    <shortcut
+        android:shortcutId="intermediateActivity"
+        android:enabled="true"
+        android:icon="@drawable/sdl-test_foreground"
+        android:shortcutShortLabel="@string/shortcutIntermediateActivityShortLabel">
+        <intent
+            android:action="android.intent.action.MAIN"
+            android:targetPackage="@ANDROID_MANIFEST_PACKAGE@"
+            android:targetClass="@[email protected]" />
+    </shortcut>
+    <!-- Specify more shortcuts here. -->
+</shortcuts>

Разница между файлами не показана из-за своего большого размера
+ 85 - 0
test/android/res/drawable/sdl-test_foreground.xml


+ 26 - 0
test/android/res/layout/arguments_layout.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingLeft="16dp"
+    android:paddingRight="16dp"
+    android:orientation="vertical" >
+    <TextView
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:inputType="textImeMultiLine|textNoSuggestions"
+        android:text="@string/label_enter_arguments" />
+    <EditText
+        android:id="@+id/arguments_edit"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:gravity="top"
+        android:hint="@string/hint_enter_arguments_here" />
+    <Button
+        android:id="@+id/arguments_start_button"
+        android:layout_width="100dp"
+        android:layout_height="wrap_content"
+        android:layout_gravity="end"
+        android:text="@string/button_start_app" />
+</LinearLayout>

+ 5 - 0
test/android/res/mipmap-anydpi-v26/sdl-test.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/sdl-test_background"/>
+    <foreground android:drawable="@drawable/sdl-test_foreground"/>
+</adaptive-icon>

+ 5 - 0
test/android/res/mipmap-anydpi-v26/sdl-test_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/sdl-test_background"/>
+    <foreground android:drawable="@drawable/sdl-test_foreground"/>
+</adaptive-icon>

BIN
test/android/res/mipmap-hdpi/sdl-test.png


BIN
test/android/res/mipmap-hdpi/sdl-test_round.png


BIN
test/android/res/mipmap-mdpi/sdl-test.png


BIN
test/android/res/mipmap-mdpi/sdl-test_round.png


BIN
test/android/res/mipmap-xhdpi/sdl-test.png


BIN
test/android/res/mipmap-xhdpi/sdl-test_round.png


BIN
test/android/res/mipmap-xxhdpi/sdl-test.png


BIN
test/android/res/mipmap-xxhdpi/sdl-test_round.png


BIN
test/android/res/mipmap-xxxhdpi/sdl-test.png


BIN
test/android/res/mipmap-xxxhdpi/sdl-test_round.png


+ 7 - 0
test/android/res/values/arg_strings.xml

@@ -0,0 +1,7 @@
+<resources>
+    <string name="label_enter_arguments">Arguments</string>
+    <string name="hint_enter_arguments_here">One argument per line.\ne.g.\n--track-mem\n--windows\n3</string>
+    <string name="button_start_app">Start</string>
+    <string name="shortcutModifyArgumentsShortLabel">Modify arguments</string>
+    <string name="shortcutIntermediateActivityShortLabel">Pass through activity</string>
+</resources>

+ 4 - 0
test/android/res/values/sdl-test_background.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="sdl-test_background">#FFFFFF</color>
+</resources>

Некоторые файлы не были показаны из-за большого количества измененных файлов