├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── app ├── src │ └── main │ │ ├── res │ │ └── values │ │ │ └── strings.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── org │ │ └── libsdl │ │ └── app │ │ └── SDLActivity.java └── build.gradle ├── .gitignore ├── UNLICENSE ├── main ├── build.gradle └── src │ └── main.cpp ├── README.md ├── gradlew.bat ├── gradlew └── SDL2 └── build.gradle /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':SDL2' 3 | include ':main' 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephen47/android-sdl2-gradle-template/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SDL Example 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | 4 | # Ignore Gradle GUI config 5 | gradle-app.setting 6 | 7 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 8 | !gradle-wrapper.jar 9 | 10 | # Cache of project 11 | .gradletasknamecache 12 | 13 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 14 | # gradle/wrapper/gradle-wrapper.properties 15 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /main/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.model.native' 2 | 3 | def lib_distribution_root = '../distribution' 4 | model { 5 | repositories { 6 | libs(PrebuiltLibraries) { 7 | SDL2 { 8 | headers.srcDir "../SDL2/include" 9 | binaries.withType(SharedLibraryBinary) { 10 | sharedLibraryFile = file("${lib_distribution_root}/SDL2/lib/${targetPlatform.getName()}/libSDL2.so") 11 | } 12 | } 13 | } 14 | } 15 | 16 | android { 17 | compileSdkVersion = 24 18 | buildToolsVersion = '23.0.3' 19 | 20 | defaultConfig { 21 | minSdkVersion.apiLevel = 13 22 | targetSdkVersion.apiLevel = 24 23 | versionCode = 1 24 | versionName = '1.0' 25 | } 26 | ndk { 27 | moduleName = 'main' 28 | cppFlags.addAll(["-I" + file("../SDL2/include/").absolutePath]) 29 | CFlags.addAll(["-I" + file("../SDL2/include/").absolutePath]) 30 | stl "stlport_static" 31 | } 32 | 33 | sources { 34 | main { 35 | jni { 36 | dependencies { 37 | library 'SDL2' linkage 'shared' 38 | } 39 | source { 40 | srcDirs 'src/' 41 | srcDirs '../SDL2/src/main/android/' 42 | } 43 | } 44 | } 45 | } 46 | 47 | } 48 | } 49 | 50 | // This is just copy out the header file and built lib into distribution 51 | // directory for clint application to use; it is a small overhead of this sample: 52 | // both lib and app are put inside one project space [save maintenance time] 53 | task(distributeLib, type : Copy) { 54 | // trigger build library 55 | dependsOn assemble 56 | into '../distribution/main/' 57 | from('build/outputs/native/release/lib') { 58 | into 'lib/' 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /main/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "SDL.h" 2 | #include 3 | 4 | int main(int argc, char* argv[]) { 5 | 6 | SDL_Window *window; // Declare a pointer 7 | 8 | SDL_Init(SDL_INIT_VIDEO); // Initialize SDL2 9 | 10 | // Create an application window with the following settings: 11 | window = SDL_CreateWindow( 12 | "An SDL2 window", // window title 13 | SDL_WINDOWPOS_UNDEFINED, // initial x position 14 | SDL_WINDOWPOS_UNDEFINED, // initial y position 15 | 640, // width, in pixels 16 | 480, // height, in pixels 17 | SDL_WINDOW_OPENGL // flags - see below 18 | ); 19 | 20 | // Check that the window was successfully created 21 | if (window == NULL) { 22 | // In the case that the window could not be made... 23 | printf("Could not create window: %s\n", SDL_GetError()); 24 | return 1; 25 | } 26 | 27 | // The window is open: could enter program loop here (see SDL_PollEvent()) 28 | // Setup renderer 29 | SDL_Renderer* renderer = NULL; 30 | renderer = SDL_CreateRenderer( window, -1, SDL_RENDERER_ACCELERATED); 31 | 32 | // Set render color to red ( background will be rendered in this color ) 33 | SDL_SetRenderDrawColor( renderer, 255, 0, 0, 255 ); 34 | 35 | // Clear winow 36 | SDL_RenderClear( renderer ); 37 | 38 | // Creat a rect at pos ( 50, 50 ) that's 50 pixels wide and 50 pixels high. 39 | SDL_Rect r; 40 | r.x = 50; 41 | r.y = 50; 42 | r.w = 50; 43 | r.h = 50; 44 | 45 | // Set render color to blue ( rect will be rendered in this color ) 46 | SDL_SetRenderDrawColor( renderer, 0, 0, 255, 255 ); 47 | 48 | // Render rect 49 | SDL_RenderFillRect( renderer, &r ); 50 | 51 | // Render the rect to the screen 52 | SDL_RenderPresent(renderer); 53 | 54 | SDL_Delay(8000); // Pause execution for 3000 milliseconds, for example 55 | 56 | // Close and destroy the window 57 | SDL_DestroyWindow(window); 58 | 59 | // Clean up 60 | SDL_Quit(); 61 | return 0; 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository is no longer maintained. 2 | 3 | This is being left up for reference but be warned it no longer builds on the modern Android toolchain. 4 | 5 | # android-sdl2-gradle-template 6 | 7 | ## Synopsis 8 | 9 | This is an example project for using libSDL2 (https://www.libsdl.org/) in an Android Gradle project. This project came about as I couldn't find a good example project using libSDL2 in a gradle project. This project will create 2 shared NDK libraries, libSDL2 and libmain, the latter containing a very basic SDL2 program which displays a blue square on a red screen. 10 | 11 | ## Requirements 12 | - JDK and JRE 8 (I have been using Debian 8 using jessie-backports to get openjdk-8-jre and openjdk-8-jdk) 13 | - Android SDK and NDK (with Android Build-tools 24.0.2 and Android Platform API 23, though these are configurable) 14 | - ANDROID_HOME and ANDROID_NDK_HOME environment variables set (I did this in /etc/environment) 15 | 16 | ## Before you begin 17 | I have not tested this in Android studio, I have just tested this using the Gradle wrapper command line. It is using gradle-experimental plugin version 0.8.0-beta3 and gradle version 2.14.1. 18 | 19 | ## Instructions 20 | 21 | Download libSDL2 from the website (https://www.libsdl.org/). Copy the src and include directories from the libSDL2 source into SDL2/ so you end up with the following directories: 22 | 23 | ``` 24 | SDL2/src 25 | SDL2/include 26 | ``` 27 | 28 | We also need to copy the example Java file from the libSDL2 source code into our project at the following location (copy from SDL2-2.0.4/android-project/src/org/libsdl/app/SDLActivity.java): 29 | 30 | ``` 31 | app/src/main/java/org/libsdl/app/SDLActivity.java 32 | ``` 33 | 34 | ## Compiling the app 35 | 36 | ``` 37 | ./gradlew :SDL2:distributeLib 38 | ``` 39 | This will compile libSDL2 as a shared library. 40 | 41 | ``` 42 | ./gradlew :main:distributeLib 43 | ``` 44 | This will compile our little project (source code in main/src/) as a shared library. You can now skip this step and just run assembleDebug as below. What I would suggest is you compile SDL2 first, commit it to source control, then just build the project as normal (assembleDebug). 45 | 46 | ``` 47 | ./gradlew assembleDebug 48 | ``` 49 | This will build our apk's and output them to app/build/outputs/apk/. 50 | 51 | ## Thanks 52 | 53 | The example libSDL2 program which draws the square on screen was found at https://stackoverflow.com/questions/21890627/drawing-a-rectangle-with-sdl2/21903973#21903973. 54 | 55 | This guide was incredibly useful while I tried to figure out how to get this working: http://lazyfoo.net/tutorials/SDL/52_hello_mobile/android_linux/index.php 56 | 57 | The Google NDK example projects were very helpful: https://github.com/googlesamples/android-ndk 58 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.model.application' 2 | 3 | def lib_distribution_root = '../distribution' 4 | 5 | gradle.projectsEvaluated { 6 | tasks.withType(JavaCompile) { 7 | options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 8 | } 9 | } 10 | 11 | model { 12 | repositories { 13 | libs(PrebuiltLibraries) { 14 | SDL2 { 15 | headers.srcDir "../SDL2/include" 16 | binaries.withType(SharedLibraryBinary) { 17 | sharedLibraryFile = file("${lib_distribution_root}/SDL2/lib/${targetPlatform.getName()}/libSDL2.so") 18 | } 19 | } 20 | main { 21 | binaries.withType(SharedLibraryBinary) { 22 | sharedLibraryFile = file("${lib_distribution_root}/main/lib/${targetPlatform.getName()}/libmain.so") 23 | } 24 | } 25 | } 26 | } 27 | 28 | android { 29 | compileSdkVersion = 23 30 | buildToolsVersion = '24.0.2' 31 | 32 | defaultConfig { 33 | applicationId = 'org.libsdl.app' 34 | minSdkVersion.apiLevel = 4 35 | targetSdkVersion.apiLevel = 23 36 | } 37 | sources { 38 | main { 39 | jni { 40 | dependencies { 41 | //library 'SDL2' linkage 'shared' 42 | project ':main' linkage 'shared' 43 | } 44 | } 45 | jniLibs { 46 | source { 47 | //srcDir "${lib_distribution_root}/SDL2/lib" 48 | srcDir "${lib_distribution_root}/main/lib" 49 | } 50 | } 51 | } 52 | } 53 | 54 | buildTypes { 55 | release { 56 | minifyEnabled = false 57 | proguardFiles.add(file('proguard-rules.txt')) 58 | } 59 | } 60 | productFlavors { 61 | // for detailed abiFilter descriptions, refer to "Supported ABIs" @ 62 | // https://developer.android.com/ndk/guides/abis.html#sa 63 | create("arm") { 64 | ndk.abiFilters.add("armeabi") 65 | } 66 | create("arm7") { 67 | ndk.abiFilters.add("armeabi-v7a") 68 | } 69 | create("arm8") { 70 | ndk.abiFilters.add("arm64-v8a") 71 | } 72 | create("x86") { 73 | ndk.abiFilters.add("x86") 74 | } 75 | create("x86-64") { 76 | ndk.abiFilters.add("x86_64") 77 | } 78 | create("mips") { 79 | ndk.abiFilters.add("mips") 80 | } 81 | create("mips-64") { 82 | ndk.abiFilters.add("mips64") 83 | } 84 | // To include all cpu architectures, leaves abiFilters empty 85 | create("all") 86 | } 87 | } 88 | } 89 | 90 | tasks.whenTaskAdded { task -> 91 | if (task.name.contains('compile')) { 92 | task.dependsOn ':main:distributeLib' 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /SDL2/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.model.native' 2 | 3 | model { 4 | android { 5 | compileSdkVersion = 24 6 | buildToolsVersion = '23.0.3' 7 | 8 | defaultConfig { 9 | minSdkVersion.apiLevel = 13 10 | targetSdkVersion.apiLevel = 24 11 | versionCode = 1 12 | versionName = '1.0' 13 | } 14 | ndk { 15 | moduleName = 'SDL2' 16 | ldLibs.addAll(["GLESv1_CM", "EGL", "GLESv2", "log", "android", "dl"]) 17 | CFlags.addAll(["-DGL_GLEXT_PROTOTYPES"]) 18 | CFlags.addAll(["-I" + file("include/").absolutePath,"-DGL_GLEXT_PROTOTYPES"]) 19 | } 20 | 21 | sources { 22 | main { 23 | jni { 24 | source { 25 | srcDir "src" 26 | exclude "audio/alsa/" 27 | exclude "audio/arts/" 28 | exclude "audio/bsd/" 29 | exclude "audio/coreaudio/" 30 | exclude "audio/directsound/" 31 | exclude "audio/disk/" 32 | exclude "audio/dsp/" 33 | exclude "audio/emscripten/" 34 | exclude "audio/esd/" 35 | exclude "audio/fusionsound/" 36 | exclude "audio/haiku/" 37 | exclude "audio/nacl/" 38 | exclude "audio/nas/" 39 | exclude "audio/psp/" 40 | exclude "audio/pulseaudio/" 41 | exclude "audio/qsa/" 42 | exclude "audio/sndio/" 43 | exclude "audio/sun/" 44 | exclude "audio/winmm/" 45 | exclude "audio/xaudio2/" 46 | exclude "core/linux/" 47 | exclude "core/windows/" 48 | exclude "core/winrt/" 49 | exclude "file/cocoa/" 50 | exclude "haptic/windows/" 51 | exclude "haptic/darwin/" 52 | exclude "haptic/linux/" 53 | exclude "libm/" 54 | exclude "main/dummy/" 55 | exclude "main/android/" 56 | exclude "main/haiku/" 57 | exclude "main/nacl/" 58 | exclude "main/psp/" 59 | exclude "main/windows/" 60 | exclude "main/winrt/" 61 | exclude "joystick/bsd/" 62 | exclude "joystick/darwin/" 63 | exclude "joystick/dummy/" 64 | exclude "joystick/emscripten/" 65 | exclude "joystick/haiku/" 66 | exclude "joystick/iphoneos/" 67 | exclude "joystick/linux/" 68 | exclude "joystick/psp/" 69 | exclude "joystick/windows/" 70 | exclude "loadso/dummy/" 71 | exclude "loadso/haiku/" 72 | exclude "loadso/windows/" 73 | exclude "power/emscripten/" 74 | exclude "power/haiku/" 75 | exclude "power/linux/" 76 | exclude "power/macosx/" 77 | exclude "power/psp/" 78 | exclude "power/uikit/" 79 | exclude "power/windows/" 80 | exclude "power/winrt/" 81 | exclude "filesystem/cocoa/" 82 | exclude "filesystem/dummy/" 83 | exclude "filesystem/emscripten/" 84 | exclude "filesystem/haiku/" 85 | exclude "filesystem/nacl/" 86 | exclude "filesystem/unix/" 87 | exclude "filesystem/windows/" 88 | exclude "filesystem/winrt/" 89 | exclude "render/direct3d/" 90 | exclude "render/direct3d11/" 91 | exclude "thread/generic/" 92 | exclude "thread/psp/" 93 | exclude "thread/stdcpp/" 94 | exclude "thread/windows/" 95 | exclude "timer/dummy/" 96 | exclude "timer/haiku/" 97 | exclude "timer/psp/" 98 | exclude "timer/windows/" 99 | exclude "video/cocoa/" 100 | exclude "video/directfb/" 101 | exclude "video/dummy/" 102 | exclude "video/emscripten/" 103 | exclude "video/haiku/" 104 | exclude "video/mir/" 105 | exclude "video/nacl/" 106 | exclude "video/pandora/" 107 | exclude "video/psp/" 108 | exclude "video/raspberry/" 109 | exclude "video/uikit/" 110 | exclude "video/vivante/" 111 | exclude "video/wayland/" 112 | exclude "video/windows/" 113 | exclude "video/winrt/" 114 | exclude "video/x11/" 115 | exclude "haptic/windows/" 116 | exclude "haptic/windows/" 117 | exclude "haptic/windows/" 118 | exclude "haptic/windows/" 119 | exclude "haptic/windows/" 120 | exclude "haptic/windows/" 121 | exclude "haptic/windows/" 122 | exclude "haptic/windows/" 123 | exclude "haptic/windows/" 124 | exclude "haptic/windows/" 125 | } 126 | } 127 | } 128 | } 129 | 130 | } 131 | } 132 | 133 | // This is just copy out the header file and built lib into distribution 134 | // directory for clint application to use; it is a small overhead of this sample: 135 | // both lib and app are put inside one project space [save maintenance time] 136 | task(distributeLib, type : Copy) { 137 | // trigger build library 138 | dependsOn assemble 139 | into '../distribution/SDL2/' 140 | from('build/outputs/native/release/lib') { 141 | into 'lib/' 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/org/libsdl/app/SDLActivity.java: -------------------------------------------------------------------------------- 1 | package org.libsdl.app; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | import java.util.Comparator; 9 | import java.util.List; 10 | import java.lang.reflect.Method; 11 | 12 | import android.app.*; 13 | import android.content.*; 14 | import android.text.InputType; 15 | import android.view.*; 16 | import android.view.inputmethod.BaseInputConnection; 17 | import android.view.inputmethod.EditorInfo; 18 | import android.view.inputmethod.InputConnection; 19 | import android.view.inputmethod.InputMethodManager; 20 | import android.widget.AbsoluteLayout; 21 | import android.widget.Button; 22 | import android.widget.LinearLayout; 23 | import android.widget.TextView; 24 | import android.os.*; 25 | import android.util.Log; 26 | import android.util.SparseArray; 27 | import android.graphics.*; 28 | import android.graphics.drawable.Drawable; 29 | import android.media.*; 30 | import android.hardware.*; 31 | import android.content.pm.ActivityInfo; 32 | 33 | /** 34 | SDL Activity 35 | */ 36 | public class SDLActivity extends Activity { 37 | private static final String TAG = "SDL"; 38 | 39 | // Keep track of the paused state 40 | public static boolean mIsPaused, mIsSurfaceReady, mHasFocus; 41 | public static boolean mExitCalledFromJava; 42 | 43 | /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ 44 | public static boolean mBrokenLibraries; 45 | 46 | // If we want to separate mouse and touch events. 47 | // This is only toggled in native code when a hint is set! 48 | public static boolean mSeparateMouseAndTouch; 49 | 50 | // Main components 51 | protected static SDLActivity mSingleton; 52 | protected static SDLSurface mSurface; 53 | protected static View mTextEdit; 54 | protected static ViewGroup mLayout; 55 | protected static SDLJoystickHandler mJoystickHandler; 56 | 57 | // This is what SDL runs in. It invokes SDL_main(), eventually 58 | protected static Thread mSDLThread; 59 | 60 | // Audio 61 | protected static AudioTrack mAudioTrack; 62 | 63 | /** 64 | * This method is called by SDL before loading the native shared libraries. 65 | * It can be overridden to provide names of shared libraries to be loaded. 66 | * The default implementation returns the defaults. It never returns null. 67 | * An array returned by a new implementation must at least contain "SDL2". 68 | * Also keep in mind that the order the libraries are loaded may matter. 69 | * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). 70 | */ 71 | protected String[] getLibraries() { 72 | return new String[] { 73 | "SDL2", 74 | // "SDL2_image", 75 | // "SDL2_mixer", 76 | // "SDL2_net", 77 | // "SDL2_ttf", 78 | "main" 79 | }; 80 | } 81 | 82 | // Load the .so 83 | public void loadLibraries() { 84 | for (String lib : getLibraries()) { 85 | System.loadLibrary(lib); 86 | } 87 | } 88 | 89 | /** 90 | * This method is called by SDL before starting the native application thread. 91 | * It can be overridden to provide the arguments after the application name. 92 | * The default implementation returns an empty array. It never returns null. 93 | * @return arguments for the native application. 94 | */ 95 | protected String[] getArguments() { 96 | return new String[0]; 97 | } 98 | 99 | public static void initialize() { 100 | // The static nature of the singleton and Android quirkyness force us to initialize everything here 101 | // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values 102 | mSingleton = null; 103 | mSurface = null; 104 | mTextEdit = null; 105 | mLayout = null; 106 | mJoystickHandler = null; 107 | mSDLThread = null; 108 | mAudioTrack = null; 109 | mExitCalledFromJava = false; 110 | mBrokenLibraries = false; 111 | mIsPaused = false; 112 | mIsSurfaceReady = false; 113 | mHasFocus = true; 114 | } 115 | 116 | // Setup 117 | @Override 118 | protected void onCreate(Bundle savedInstanceState) { 119 | Log.v(TAG, "Device: " + android.os.Build.DEVICE); 120 | Log.v(TAG, "Model: " + android.os.Build.MODEL); 121 | Log.v(TAG, "onCreate(): " + mSingleton); 122 | super.onCreate(savedInstanceState); 123 | 124 | SDLActivity.initialize(); 125 | // So we can call stuff from static callbacks 126 | mSingleton = this; 127 | 128 | // Load shared libraries 129 | String errorMsgBrokenLib = ""; 130 | try { 131 | loadLibraries(); 132 | } catch(UnsatisfiedLinkError e) { 133 | System.err.println(e.getMessage()); 134 | mBrokenLibraries = true; 135 | errorMsgBrokenLib = e.getMessage(); 136 | } catch(Exception e) { 137 | System.err.println(e.getMessage()); 138 | mBrokenLibraries = true; 139 | errorMsgBrokenLib = e.getMessage(); 140 | } 141 | 142 | if (mBrokenLibraries) 143 | { 144 | AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); 145 | dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." 146 | + System.getProperty("line.separator") 147 | + System.getProperty("line.separator") 148 | + "Error: " + errorMsgBrokenLib); 149 | dlgAlert.setTitle("SDL Error"); 150 | dlgAlert.setPositiveButton("Exit", 151 | new DialogInterface.OnClickListener() { 152 | @Override 153 | public void onClick(DialogInterface dialog,int id) { 154 | // if this button is clicked, close current activity 155 | SDLActivity.mSingleton.finish(); 156 | } 157 | }); 158 | dlgAlert.setCancelable(false); 159 | dlgAlert.create().show(); 160 | 161 | return; 162 | } 163 | 164 | // Set up the surface 165 | mSurface = new SDLSurface(getApplication()); 166 | 167 | if(Build.VERSION.SDK_INT >= 12) { 168 | mJoystickHandler = new SDLJoystickHandler_API12(); 169 | } 170 | else { 171 | mJoystickHandler = new SDLJoystickHandler(); 172 | } 173 | 174 | mLayout = new AbsoluteLayout(this); 175 | mLayout.addView(mSurface); 176 | 177 | setContentView(mLayout); 178 | 179 | // Get filename from "Open with" of another application 180 | Intent intent = getIntent(); 181 | 182 | if (intent != null && intent.getData() != null) { 183 | String filename = intent.getData().getPath(); 184 | if (filename != null) { 185 | Log.v(TAG, "Got filename: " + filename); 186 | SDLActivity.onNativeDropFile(filename); 187 | } 188 | } 189 | } 190 | 191 | // Events 192 | @Override 193 | protected void onPause() { 194 | Log.v(TAG, "onPause()"); 195 | super.onPause(); 196 | 197 | if (SDLActivity.mBrokenLibraries) { 198 | return; 199 | } 200 | 201 | SDLActivity.handlePause(); 202 | } 203 | 204 | @Override 205 | protected void onResume() { 206 | Log.v(TAG, "onResume()"); 207 | super.onResume(); 208 | 209 | if (SDLActivity.mBrokenLibraries) { 210 | return; 211 | } 212 | 213 | SDLActivity.handleResume(); 214 | } 215 | 216 | 217 | @Override 218 | public void onWindowFocusChanged(boolean hasFocus) { 219 | super.onWindowFocusChanged(hasFocus); 220 | Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); 221 | 222 | if (SDLActivity.mBrokenLibraries) { 223 | return; 224 | } 225 | 226 | SDLActivity.mHasFocus = hasFocus; 227 | if (hasFocus) { 228 | SDLActivity.handleResume(); 229 | } 230 | } 231 | 232 | @Override 233 | public void onLowMemory() { 234 | Log.v(TAG, "onLowMemory()"); 235 | super.onLowMemory(); 236 | 237 | if (SDLActivity.mBrokenLibraries) { 238 | return; 239 | } 240 | 241 | SDLActivity.nativeLowMemory(); 242 | } 243 | 244 | @Override 245 | protected void onDestroy() { 246 | Log.v(TAG, "onDestroy()"); 247 | 248 | if (SDLActivity.mBrokenLibraries) { 249 | super.onDestroy(); 250 | // Reset everything in case the user re opens the app 251 | SDLActivity.initialize(); 252 | return; 253 | } 254 | 255 | // Send a quit message to the application 256 | SDLActivity.mExitCalledFromJava = true; 257 | SDLActivity.nativeQuit(); 258 | 259 | // Now wait for the SDL thread to quit 260 | if (SDLActivity.mSDLThread != null) { 261 | try { 262 | SDLActivity.mSDLThread.join(); 263 | } catch(Exception e) { 264 | Log.v(TAG, "Problem stopping thread: " + e); 265 | } 266 | SDLActivity.mSDLThread = null; 267 | 268 | //Log.v(TAG, "Finished waiting for SDL thread"); 269 | } 270 | 271 | super.onDestroy(); 272 | // Reset everything in case the user re opens the app 273 | SDLActivity.initialize(); 274 | } 275 | 276 | @Override 277 | public boolean dispatchKeyEvent(KeyEvent event) { 278 | 279 | if (SDLActivity.mBrokenLibraries) { 280 | return false; 281 | } 282 | 283 | int keyCode = event.getKeyCode(); 284 | // Ignore certain special keys so they're handled by Android 285 | if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || 286 | keyCode == KeyEvent.KEYCODE_VOLUME_UP || 287 | keyCode == KeyEvent.KEYCODE_CAMERA || 288 | keyCode == 168 || /* API 11: KeyEvent.KEYCODE_ZOOM_IN */ 289 | keyCode == 169 /* API 11: KeyEvent.KEYCODE_ZOOM_OUT */ 290 | ) { 291 | return false; 292 | } 293 | return super.dispatchKeyEvent(event); 294 | } 295 | 296 | /** Called by onPause or surfaceDestroyed. Even if surfaceDestroyed 297 | * is the first to be called, mIsSurfaceReady should still be set 298 | * to 'true' during the call to onPause (in a usual scenario). 299 | */ 300 | public static void handlePause() { 301 | if (!SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady) { 302 | SDLActivity.mIsPaused = true; 303 | SDLActivity.nativePause(); 304 | mSurface.handlePause(); 305 | } 306 | } 307 | 308 | /** Called by onResume or surfaceCreated. An actual resume should be done only when the surface is ready. 309 | * Note: Some Android variants may send multiple surfaceChanged events, so we don't need to resume 310 | * every time we get one of those events, only if it comes after surfaceDestroyed 311 | */ 312 | public static void handleResume() { 313 | if (SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady && SDLActivity.mHasFocus) { 314 | SDLActivity.mIsPaused = false; 315 | SDLActivity.nativeResume(); 316 | mSurface.handleResume(); 317 | } 318 | } 319 | 320 | /* The native thread has finished */ 321 | public static void handleNativeExit() { 322 | SDLActivity.mSDLThread = null; 323 | mSingleton.finish(); 324 | } 325 | 326 | 327 | // Messages from the SDLMain thread 328 | static final int COMMAND_CHANGE_TITLE = 1; 329 | static final int COMMAND_UNUSED = 2; 330 | static final int COMMAND_TEXTEDIT_HIDE = 3; 331 | static final int COMMAND_SET_KEEP_SCREEN_ON = 5; 332 | 333 | protected static final int COMMAND_USER = 0x8000; 334 | 335 | /** 336 | * This method is called by SDL if SDL did not handle a message itself. 337 | * This happens if a received message contains an unsupported command. 338 | * Method can be overwritten to handle Messages in a different class. 339 | * @param command the command of the message. 340 | * @param param the parameter of the message. May be null. 341 | * @return if the message was handled in overridden method. 342 | */ 343 | protected boolean onUnhandledMessage(int command, Object param) { 344 | return false; 345 | } 346 | 347 | /** 348 | * A Handler class for Messages from native SDL applications. 349 | * It uses current Activities as target (e.g. for the title). 350 | * static to prevent implicit references to enclosing object. 351 | */ 352 | protected static class SDLCommandHandler extends Handler { 353 | @Override 354 | public void handleMessage(Message msg) { 355 | Context context = getContext(); 356 | if (context == null) { 357 | Log.e(TAG, "error handling message, getContext() returned null"); 358 | return; 359 | } 360 | switch (msg.arg1) { 361 | case COMMAND_CHANGE_TITLE: 362 | if (context instanceof Activity) { 363 | ((Activity) context).setTitle((String)msg.obj); 364 | } else { 365 | Log.e(TAG, "error handling message, getContext() returned no Activity"); 366 | } 367 | break; 368 | case COMMAND_TEXTEDIT_HIDE: 369 | if (mTextEdit != null) { 370 | mTextEdit.setVisibility(View.GONE); 371 | 372 | InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); 373 | imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); 374 | } 375 | break; 376 | case COMMAND_SET_KEEP_SCREEN_ON: 377 | { 378 | Window window = ((Activity) context).getWindow(); 379 | if (window != null) { 380 | if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) { 381 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 382 | } else { 383 | window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 384 | } 385 | } 386 | break; 387 | } 388 | default: 389 | if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { 390 | Log.e(TAG, "error handling message, command is " + msg.arg1); 391 | } 392 | } 393 | } 394 | } 395 | 396 | // Handler for the messages 397 | Handler commandHandler = new SDLCommandHandler(); 398 | 399 | // Send a message from the SDLMain thread 400 | boolean sendCommand(int command, Object data) { 401 | Message msg = commandHandler.obtainMessage(); 402 | msg.arg1 = command; 403 | msg.obj = data; 404 | return commandHandler.sendMessage(msg); 405 | } 406 | 407 | // C functions we call 408 | public static native int nativeInit(Object arguments); 409 | public static native void nativeLowMemory(); 410 | public static native void nativeQuit(); 411 | public static native void nativePause(); 412 | public static native void nativeResume(); 413 | public static native void onNativeDropFile(String filename); 414 | public static native void onNativeResize(int x, int y, int format, float rate); 415 | public static native int onNativePadDown(int device_id, int keycode); 416 | public static native int onNativePadUp(int device_id, int keycode); 417 | public static native void onNativeJoy(int device_id, int axis, 418 | float value); 419 | public static native void onNativeHat(int device_id, int hat_id, 420 | int x, int y); 421 | public static native void onNativeKeyDown(int keycode); 422 | public static native void onNativeKeyUp(int keycode); 423 | public static native void onNativeKeyboardFocusLost(); 424 | public static native void onNativeMouse(int button, int action, float x, float y); 425 | public static native void onNativeTouch(int touchDevId, int pointerFingerId, 426 | int action, float x, 427 | float y, float p); 428 | public static native void onNativeAccel(float x, float y, float z); 429 | public static native void onNativeSurfaceChanged(); 430 | public static native void onNativeSurfaceDestroyed(); 431 | public static native int nativeAddJoystick(int device_id, String name, 432 | int is_accelerometer, int nbuttons, 433 | int naxes, int nhats, int nballs); 434 | public static native int nativeRemoveJoystick(int device_id); 435 | public static native String nativeGetHint(String name); 436 | 437 | /** 438 | * This method is called by SDL using JNI. 439 | */ 440 | public static boolean setActivityTitle(String title) { 441 | // Called from SDLMain() thread and can't directly affect the view 442 | return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); 443 | } 444 | 445 | /** 446 | * This method is called by SDL using JNI. 447 | */ 448 | public static boolean sendMessage(int command, int param) { 449 | return mSingleton.sendCommand(command, Integer.valueOf(param)); 450 | } 451 | 452 | /** 453 | * This method is called by SDL using JNI. 454 | */ 455 | public static Context getContext() { 456 | return mSingleton; 457 | } 458 | 459 | /** 460 | * This method is called by SDL using JNI. 461 | * @return result of getSystemService(name) but executed on UI thread. 462 | */ 463 | public Object getSystemServiceFromUiThread(final String name) { 464 | final Object lock = new Object(); 465 | final Object[] results = new Object[2]; // array for writable variables 466 | synchronized (lock) { 467 | runOnUiThread(new Runnable() { 468 | @Override 469 | public void run() { 470 | synchronized (lock) { 471 | results[0] = getSystemService(name); 472 | results[1] = Boolean.TRUE; 473 | lock.notify(); 474 | } 475 | } 476 | }); 477 | if (results[1] == null) { 478 | try { 479 | lock.wait(); 480 | } catch (InterruptedException ex) { 481 | ex.printStackTrace(); 482 | } 483 | } 484 | } 485 | return results[0]; 486 | } 487 | 488 | static class ShowTextInputTask implements Runnable { 489 | /* 490 | * This is used to regulate the pan&scan method to have some offset from 491 | * the bottom edge of the input region and the top edge of an input 492 | * method (soft keyboard) 493 | */ 494 | static final int HEIGHT_PADDING = 15; 495 | 496 | public int x, y, w, h; 497 | 498 | public ShowTextInputTask(int x, int y, int w, int h) { 499 | this.x = x; 500 | this.y = y; 501 | this.w = w; 502 | this.h = h; 503 | } 504 | 505 | @Override 506 | public void run() { 507 | AbsoluteLayout.LayoutParams params = new AbsoluteLayout.LayoutParams( 508 | w, h + HEIGHT_PADDING, x, y); 509 | 510 | if (mTextEdit == null) { 511 | mTextEdit = new DummyEdit(getContext()); 512 | 513 | mLayout.addView(mTextEdit, params); 514 | } else { 515 | mTextEdit.setLayoutParams(params); 516 | } 517 | 518 | mTextEdit.setVisibility(View.VISIBLE); 519 | mTextEdit.requestFocus(); 520 | 521 | InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 522 | imm.showSoftInput(mTextEdit, 0); 523 | } 524 | } 525 | 526 | /** 527 | * This method is called by SDL using JNI. 528 | */ 529 | public static boolean showTextInput(int x, int y, int w, int h) { 530 | // Transfer the task to the main thread as a Runnable 531 | return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); 532 | } 533 | 534 | /** 535 | * This method is called by SDL using JNI. 536 | */ 537 | public static Surface getNativeSurface() { 538 | return SDLActivity.mSurface.getNativeSurface(); 539 | } 540 | 541 | // Audio 542 | 543 | /** 544 | * This method is called by SDL using JNI. 545 | */ 546 | public static int audioInit(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) { 547 | int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO; 548 | int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT; 549 | int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1); 550 | 551 | Log.v(TAG, "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer"); 552 | 553 | // Let the user pick a larger buffer if they really want -- but ye 554 | // gods they probably shouldn't, the minimums are horrifyingly high 555 | // latency already 556 | desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize); 557 | 558 | if (mAudioTrack == null) { 559 | mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, 560 | channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); 561 | 562 | // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid 563 | // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java 564 | // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() 565 | 566 | if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { 567 | Log.e(TAG, "Failed during initialization of Audio Track"); 568 | mAudioTrack = null; 569 | return -1; 570 | } 571 | 572 | mAudioTrack.play(); 573 | } 574 | 575 | Log.v(TAG, "SDL audio: got " + ((mAudioTrack.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioTrack.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + (mAudioTrack.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer"); 576 | 577 | return 0; 578 | } 579 | 580 | /** 581 | * This method is called by SDL using JNI. 582 | */ 583 | public static void audioWriteShortBuffer(short[] buffer) { 584 | for (int i = 0; i < buffer.length; ) { 585 | int result = mAudioTrack.write(buffer, i, buffer.length - i); 586 | if (result > 0) { 587 | i += result; 588 | } else if (result == 0) { 589 | try { 590 | Thread.sleep(1); 591 | } catch(InterruptedException e) { 592 | // Nom nom 593 | } 594 | } else { 595 | Log.w(TAG, "SDL audio: error return from write(short)"); 596 | return; 597 | } 598 | } 599 | } 600 | 601 | /** 602 | * This method is called by SDL using JNI. 603 | */ 604 | public static void audioWriteByteBuffer(byte[] buffer) { 605 | for (int i = 0; i < buffer.length; ) { 606 | int result = mAudioTrack.write(buffer, i, buffer.length - i); 607 | if (result > 0) { 608 | i += result; 609 | } else if (result == 0) { 610 | try { 611 | Thread.sleep(1); 612 | } catch(InterruptedException e) { 613 | // Nom nom 614 | } 615 | } else { 616 | Log.w(TAG, "SDL audio: error return from write(byte)"); 617 | return; 618 | } 619 | } 620 | } 621 | 622 | /** 623 | * This method is called by SDL using JNI. 624 | */ 625 | public static void audioQuit() { 626 | if (mAudioTrack != null) { 627 | mAudioTrack.stop(); 628 | mAudioTrack = null; 629 | } 630 | } 631 | 632 | // Input 633 | 634 | /** 635 | * This method is called by SDL using JNI. 636 | * @return an array which may be empty but is never null. 637 | */ 638 | public static int[] inputGetInputDeviceIds(int sources) { 639 | int[] ids = InputDevice.getDeviceIds(); 640 | int[] filtered = new int[ids.length]; 641 | int used = 0; 642 | for (int i = 0; i < ids.length; ++i) { 643 | InputDevice device = InputDevice.getDevice(ids[i]); 644 | if ((device != null) && ((device.getSources() & sources) != 0)) { 645 | filtered[used++] = device.getId(); 646 | } 647 | } 648 | return Arrays.copyOf(filtered, used); 649 | } 650 | 651 | // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance 652 | public static boolean handleJoystickMotionEvent(MotionEvent event) { 653 | return mJoystickHandler.handleMotionEvent(event); 654 | } 655 | 656 | /** 657 | * This method is called by SDL using JNI. 658 | */ 659 | public static void pollInputDevices() { 660 | if (SDLActivity.mSDLThread != null) { 661 | mJoystickHandler.pollInputDevices(); 662 | } 663 | } 664 | 665 | // APK expansion files support 666 | 667 | /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */ 668 | private Object expansionFile; 669 | 670 | /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */ 671 | private Method expansionFileMethod; 672 | 673 | /** 674 | * This method was called by SDL using JNI. 675 | * @deprecated because of an incorrect name 676 | */ 677 | @Deprecated 678 | public InputStream openAPKExtensionInputStream(String fileName) throws IOException { 679 | return openAPKExpansionInputStream(fileName); 680 | } 681 | 682 | /** 683 | * This method is called by SDL using JNI. 684 | * @return an InputStream on success or null if no expansion file was used. 685 | * @throws IOException on errors. Message is set for the SDL error message. 686 | */ 687 | public InputStream openAPKExpansionInputStream(String fileName) throws IOException { 688 | // Get a ZipResourceFile representing a merger of both the main and patch files 689 | if (expansionFile == null) { 690 | String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION"); 691 | if (mainHint == null) { 692 | return null; // no expansion use if no main version was set 693 | } 694 | String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION"); 695 | if (patchHint == null) { 696 | return null; // no expansion use if no patch version was set 697 | } 698 | 699 | Integer mainVersion; 700 | Integer patchVersion; 701 | try { 702 | mainVersion = Integer.valueOf(mainHint); 703 | patchVersion = Integer.valueOf(patchHint); 704 | } catch (NumberFormatException ex) { 705 | ex.printStackTrace(); 706 | throw new IOException("No valid file versions set for APK expansion files", ex); 707 | } 708 | 709 | try { 710 | // To avoid direct dependency on Google APK expansion library that is 711 | // not a part of Android SDK we access it using reflection 712 | expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport") 713 | .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class) 714 | .invoke(null, this, mainVersion, patchVersion); 715 | 716 | expansionFileMethod = expansionFile.getClass() 717 | .getMethod("getInputStream", String.class); 718 | } catch (Exception ex) { 719 | ex.printStackTrace(); 720 | expansionFile = null; 721 | expansionFileMethod = null; 722 | throw new IOException("Could not access APK expansion support library", ex); 723 | } 724 | } 725 | 726 | // Get an input stream for a known file inside the expansion file ZIPs 727 | InputStream fileStream; 728 | try { 729 | fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName); 730 | } catch (Exception ex) { 731 | // calling "getInputStream" failed 732 | ex.printStackTrace(); 733 | throw new IOException("Could not open stream from APK expansion file", ex); 734 | } 735 | 736 | if (fileStream == null) { 737 | // calling "getInputStream" was successful but null was returned 738 | throw new IOException("Could not find path in APK expansion file"); 739 | } 740 | 741 | return fileStream; 742 | } 743 | 744 | // Messagebox 745 | 746 | /** Result of current messagebox. Also used for blocking the calling thread. */ 747 | protected final int[] messageboxSelection = new int[1]; 748 | 749 | /** Id of current dialog. */ 750 | protected int dialogs = 0; 751 | 752 | /** 753 | * This method is called by SDL using JNI. 754 | * Shows the messagebox from UI thread and block calling thread. 755 | * buttonFlags, buttonIds and buttonTexts must have same length. 756 | * @param buttonFlags array containing flags for every button. 757 | * @param buttonIds array containing id for every button. 758 | * @param buttonTexts array containing text for every button. 759 | * @param colors null for default or array of length 5 containing colors. 760 | * @return button id or -1. 761 | */ 762 | public int messageboxShowMessageBox( 763 | final int flags, 764 | final String title, 765 | final String message, 766 | final int[] buttonFlags, 767 | final int[] buttonIds, 768 | final String[] buttonTexts, 769 | final int[] colors) { 770 | 771 | messageboxSelection[0] = -1; 772 | 773 | // sanity checks 774 | 775 | if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { 776 | return -1; // implementation broken 777 | } 778 | 779 | // collect arguments for Dialog 780 | 781 | final Bundle args = new Bundle(); 782 | args.putInt("flags", flags); 783 | args.putString("title", title); 784 | args.putString("message", message); 785 | args.putIntArray("buttonFlags", buttonFlags); 786 | args.putIntArray("buttonIds", buttonIds); 787 | args.putStringArray("buttonTexts", buttonTexts); 788 | args.putIntArray("colors", colors); 789 | 790 | // trigger Dialog creation on UI thread 791 | 792 | runOnUiThread(new Runnable() { 793 | @Override 794 | public void run() { 795 | showDialog(dialogs++, args); 796 | } 797 | }); 798 | 799 | // block the calling thread 800 | 801 | synchronized (messageboxSelection) { 802 | try { 803 | messageboxSelection.wait(); 804 | } catch (InterruptedException ex) { 805 | ex.printStackTrace(); 806 | return -1; 807 | } 808 | } 809 | 810 | // return selected value 811 | 812 | return messageboxSelection[0]; 813 | } 814 | 815 | @Override 816 | protected Dialog onCreateDialog(int ignore, Bundle args) { 817 | 818 | // TODO set values from "flags" to messagebox dialog 819 | 820 | // get colors 821 | 822 | int[] colors = args.getIntArray("colors"); 823 | int backgroundColor; 824 | int textColor; 825 | int buttonBorderColor; 826 | int buttonBackgroundColor; 827 | int buttonSelectedColor; 828 | if (colors != null) { 829 | int i = -1; 830 | backgroundColor = colors[++i]; 831 | textColor = colors[++i]; 832 | buttonBorderColor = colors[++i]; 833 | buttonBackgroundColor = colors[++i]; 834 | buttonSelectedColor = colors[++i]; 835 | } else { 836 | backgroundColor = Color.TRANSPARENT; 837 | textColor = Color.TRANSPARENT; 838 | buttonBorderColor = Color.TRANSPARENT; 839 | buttonBackgroundColor = Color.TRANSPARENT; 840 | buttonSelectedColor = Color.TRANSPARENT; 841 | } 842 | 843 | // create dialog with title and a listener to wake up calling thread 844 | 845 | final Dialog dialog = new Dialog(this); 846 | dialog.setTitle(args.getString("title")); 847 | dialog.setCancelable(false); 848 | dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { 849 | @Override 850 | public void onDismiss(DialogInterface unused) { 851 | synchronized (messageboxSelection) { 852 | messageboxSelection.notify(); 853 | } 854 | } 855 | }); 856 | 857 | // create text 858 | 859 | TextView message = new TextView(this); 860 | message.setGravity(Gravity.CENTER); 861 | message.setText(args.getString("message")); 862 | if (textColor != Color.TRANSPARENT) { 863 | message.setTextColor(textColor); 864 | } 865 | 866 | // create buttons 867 | 868 | int[] buttonFlags = args.getIntArray("buttonFlags"); 869 | int[] buttonIds = args.getIntArray("buttonIds"); 870 | String[] buttonTexts = args.getStringArray("buttonTexts"); 871 | 872 | final SparseArray