From feeeabcbf6696f27ddd68ad56d19db1660a6c4fe Mon Sep 17 00:00:00 2001 From: Holger Schemel Date: Fri, 3 Jan 2020 01:34:36 +0100 Subject: [PATCH] added Android project files To build Rocks'n'Diamonds for Android, the SDL library source files and the assets directory must be added to the project. See file "build-projects/android/README.txt" for details. --- Makefile | 2 +- .../android/AndroidManifest.xml.tmpl | 36 + build-projects/android/Makefile | 56 + build-projects/android/README.txt | 27 + build-projects/android/ant.properties | 17 + .../build-scripts/create_asset_tocs.sh | 28 + .../android/build-scripts/create_manifest.sh | 43 + build-projects/android/build.properties | 17 + build-projects/android/build.xml | 92 + build-projects/android/default.properties | 11 + build-projects/android/jni/Android.mk | 1 + build-projects/android/jni/Application.mk | 9 + build-projects/android/jni/src | 1 + build-projects/android/project.properties | 14 + .../android/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 3041 bytes .../android/res/drawable-ldpi/ic_launcher.png | Bin 0 -> 1522 bytes .../android/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2349 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 4450 bytes .../res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 8823 bytes build-projects/android/res/layout/main.xml | 12 + build-projects/android/res/values/strings.xml | 4 + .../rocksndiamonds/RocksNDiamonds.java | 6 + .../src/org/libsdl/app/SDLActivity.java | 1743 +++++++++++++++++ src/Android.mk | 8 +- 24 files changed, 2122 insertions(+), 5 deletions(-) create mode 100644 build-projects/android/AndroidManifest.xml.tmpl create mode 100644 build-projects/android/Makefile create mode 100644 build-projects/android/README.txt create mode 100644 build-projects/android/ant.properties create mode 100755 build-projects/android/build-scripts/create_asset_tocs.sh create mode 100755 build-projects/android/build-scripts/create_manifest.sh create mode 100644 build-projects/android/build.properties create mode 100644 build-projects/android/build.xml create mode 100644 build-projects/android/default.properties create mode 100644 build-projects/android/jni/Android.mk create mode 100644 build-projects/android/jni/Application.mk create mode 120000 build-projects/android/jni/src create mode 100644 build-projects/android/project.properties create mode 100644 build-projects/android/res/drawable-hdpi/ic_launcher.png create mode 100644 build-projects/android/res/drawable-ldpi/ic_launcher.png create mode 100644 build-projects/android/res/drawable-mdpi/ic_launcher.png create mode 100644 build-projects/android/res/drawable-xhdpi/ic_launcher.png create mode 100644 build-projects/android/res/drawable-xxhdpi/ic_launcher.png create mode 100644 build-projects/android/res/layout/main.xml create mode 100644 build-projects/android/res/values/strings.xml create mode 100644 build-projects/android/src/org/artsoft/rocksndiamonds/RocksNDiamonds.java create mode 100644 build-projects/android/src/org/libsdl/app/SDLActivity.java diff --git a/Makefile b/Makefile index 68745a5d..9dbdfe2b 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ CROSS_PATH_WIN32 = /usr/local/cross-tools/i386-mingw32msvc .EXPORT_ALL_VARIABLES: SRC_DIR = src -ANDROID_DIR ?= android +ANDROID_DIR ?= build-projects/android MAKE_CMD = $(MAKE) -C $(SRC_DIR) MAKE_CMD_ANDROID = $(MAKE) -C $(ANDROID_DIR) diff --git a/build-projects/android/AndroidManifest.xml.tmpl b/build-projects/android/AndroidManifest.xml.tmpl new file mode 100644 index 00000000..2df66ffe --- /dev/null +++ b/build-projects/android/AndroidManifest.xml.tmpl @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build-projects/android/Makefile b/build-projects/android/Makefile new file mode 100644 index 00000000..0bf05df3 --- /dev/null +++ b/build-projects/android/Makefile @@ -0,0 +1,56 @@ +# ----------------------------------------------------------------------------- +# Rocks'n'Diamonds for Android +# ----------------------------------------------------------------------------- + +# debugging C/ASM: +# arm-linux-androideabi-objdump -S obj/local/armeabi-v7a/libmain.so + +PROGBASE = rocksndiamonds + +ANDROID_SDK_PATH := ${HOME}/projects/Android/SDK/android-sdk +ANDROID_NDK_PATH := ${HOME}/projects/Android/SDK/android-ndk + +SDK_TOOLS_PATH := ${ANDROID_SDK_PATH}/tools +SDK_PLATFORM_TOOLS_PATH := ${ANDROID_SDK_PATH}/platform-tools +NDK_PATH := ${ANDROID_NDK_PATH} + +PATH := ${PATH}:${SDK_TOOLS_PATH}:${SDK_PLATFORM_TOOLS_PATH}:${NDK_PATH} + +AUTO_GENERATED_DIRS = bin gen obj + + +all: package + +manifest: + ./build-scripts/create_manifest.sh + +compile: manifest + ndk-build NDK_DEBUG=1 + +verbose: manifest + ndk-build V=1 + +package: compile + ANDROID_AAPT_IGNORE="__IGNORE_NOTHING__" ant debug + +install: package + adb install -r bin/$(PROGBASE)-debug.apk + +ant-init: + android update project --name $(PROGBASE) --path . + +assets-toc: + ./build-scripts/create_asset_tocs.sh assets + +clean-build: + ndk-build clean + +clean-auto-generated: + rm -rf $(AUTO_GENERATED_DIRS) + +clean: clean-build clean-auto-generated + +dist-clean: clean + +coredump: + adb logcat -d | ndk-stack -sym obj/local/armeabi-v7a diff --git a/build-projects/android/README.txt b/build-projects/android/README.txt new file mode 100644 index 00000000..8a2e68dc --- /dev/null +++ b/build-projects/android/README.txt @@ -0,0 +1,27 @@ +# ----------------------------------------------------------------------------- +# Rocks'n'Diamonds for Android +# ----------------------------------------------------------------------------- + +Add the following content to build Rocks'n'Diamonds for Android: + +Download and extract the following SDL libraries to the "jni" directory: + +- jni/SDL2 +- jni/SDL2_image +- jni/SDL2_mixer +- jni/SDL2_net +- jni/smpeg2 + +Copy the following repository directories to the "assets" directory: + +- assets/conf +- assets/docs +- assets/graphics +- assets/levels +- assets/music +- assets/sounds + +Create the file "local.properties" by executing "make ant-init". + +When done, use "make" (or "make android" from the repository's root directory) +to compile and package the final Rocks'n'Diamonds APK file. diff --git a/build-projects/android/ant.properties b/build-projects/android/ant.properties new file mode 100644 index 00000000..b0971e89 --- /dev/null +++ b/build-projects/android/ant.properties @@ -0,0 +1,17 @@ +# This file is used to override default values used by the Ant build system. +# +# This file must be checked into Version Control Systems, as it is +# integral to the build system of your project. + +# This file is only used by the Ant script. + +# You can use this to override default values such as +# 'source.dir' for the location of your java source folder and +# 'out.dir' for the location of your output folder. + +# You can also use it define how the release builds are signed by declaring +# the following properties: +# 'key.store' for the location of your keystore and +# 'key.alias' for the name of the key to use. +# The password will be asked during the build when you use the 'release' target. + diff --git a/build-projects/android/build-scripts/create_asset_tocs.sh b/build-projects/android/build-scripts/create_asset_tocs.sh new file mode 100755 index 00000000..936078e0 --- /dev/null +++ b/build-projects/android/build-scripts/create_asset_tocs.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +ASSETS_PATH="$1" + +TOC_BASENAME=".toc" +TOC_FILENAME="$ASSETS_PATH/$TOC_BASENAME" + +if [ "$ASSETS_PATH" = "" ]; then + echo "Usage: $0 " + + exit 5 +fi + +if [ ! -d "$ASSETS_PATH" ]; then + echo "ERROR: cannot find directory '$ASSETS_PATH'!" + + exit 10 +fi + +ls -1Ap "$ASSETS_PATH" | grep -v "^$TOC_BASENAME$" > "$TOC_FILENAME" + +for i in "$ASSETS_PATH"/*; do + if [ -d "$i" ]; then + $0 "$i" + fi +done + +exit 0 diff --git a/build-projects/android/build-scripts/create_manifest.sh b/build-projects/android/build-scripts/create_manifest.sh new file mode 100755 index 00000000..a1e4e98f --- /dev/null +++ b/build-projects/android/build-scripts/create_manifest.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +BASE_DIR="jni" + +MANIFEST_FILE="AndroidManifest.xml" +MANIFEST_TMPL="$MANIFEST_FILE.tmpl" + +MAIN_H="$BASE_DIR/src/main.h" + +VERSION_SUPER=`grep "#define PROGRAM_VERSION_SUPER" $MAIN_H | awk '{print $3}'` +VERSION_MAJOR=`grep "#define PROGRAM_VERSION_MAJOR" $MAIN_H | awk '{print $3}'` +VERSION_MINOR=`grep "#define PROGRAM_VERSION_MINOR" $MAIN_H | awk '{print $3}'` +VERSION_PATCH=`grep "#define PROGRAM_VERSION_PATCH" $MAIN_H | awk '{print $3}'` +VERSION_EXTRA=`grep "#define PROGRAM_VERSION_EXTRA" $MAIN_H \ + | awk -F\" '{print $2}' \ + | tr '[A-Z ]' '[a-z-]'` + +UNIQUE_VERSION=`echo "$VERSION_SUPER" | wc -l | awk '{ print $1 }'` +if [ "$UNIQUE_VERSION" != "1" ]; then + echo "ERROR: program version number ('PROGRAM_VERSION_SUPER') not unique!" + exit +fi + +VERSION_NAME="$VERSION_SUPER.$VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH" +VERSION_CODE=$(printf '%d%02d%02d%02d' \ + "$VERSION_SUPER" \ + "$VERSION_MAJOR" \ + "$VERSION_MINOR" \ + "$VERSION_PATCH") + +if [ "$VERSION_EXTRA" != "" ]; then + VERSION_NAME="$VERSION_NAME$VERSION_EXTRA" +fi + +# echo "::: VERSION_NAME == '$VERSION_NAME'" +# echo "::: VERSION_CODE == '$VERSION_CODE'" + +cat "$MANIFEST_TMPL" \ + | sed -e "s/__VERSION_NAME__/$VERSION_NAME/" \ + | sed -e "s/__VERSION_CODE__/$VERSION_CODE/" \ + > "$MANIFEST_FILE" + +exit 0 diff --git a/build-projects/android/build.properties b/build-projects/android/build.properties new file mode 100644 index 00000000..ee52d86d --- /dev/null +++ b/build-projects/android/build.properties @@ -0,0 +1,17 @@ +# This file is used to override default values used by the Ant build system. +# +# This file must be checked in Version Control Systems, as it is +# integral to the build system of your project. + +# This file is only used by the Ant script. + +# You can use this to override default values such as +# 'source.dir' for the location of your java source folder and +# 'out.dir' for the location of your output folder. + +# You can also use it define how the release builds are signed by declaring +# the following properties: +# 'key.store' for the location of your keystore and +# 'key.alias' for the name of the key to use. +# The password will be asked during the build when you use the 'release' target. + diff --git a/build-projects/android/build.xml b/build-projects/android/build.xml new file mode 100644 index 00000000..f8c6185f --- /dev/null +++ b/build-projects/android/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build-projects/android/default.properties b/build-projects/android/default.properties new file mode 100644 index 00000000..3ac25234 --- /dev/null +++ b/build-projects/android/default.properties @@ -0,0 +1,11 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-12 diff --git a/build-projects/android/jni/Android.mk b/build-projects/android/jni/Android.mk new file mode 100644 index 00000000..5053e7d6 --- /dev/null +++ b/build-projects/android/jni/Android.mk @@ -0,0 +1 @@ +include $(call all-subdir-makefiles) diff --git a/build-projects/android/jni/Application.mk b/build-projects/android/jni/Application.mk new file mode 100644 index 00000000..e5b9a1b7 --- /dev/null +++ b/build-projects/android/jni/Application.mk @@ -0,0 +1,9 @@ + +# Uncomment this if you're using STL in your project +# See CPLUSPLUS-SUPPORT.html in the NDK documentation for more information +# APP_STL := stlport_static + +APP_ABI := armeabi-v7a armeabi x86 +APP_STL := gnustl_static + +APP_OPTIM := debug diff --git a/build-projects/android/jni/src b/build-projects/android/jni/src new file mode 120000 index 00000000..dabb0e15 --- /dev/null +++ b/build-projects/android/jni/src @@ -0,0 +1 @@ +../../../src \ No newline at end of file diff --git a/build-projects/android/project.properties b/build-projects/android/project.properties new file mode 100644 index 00000000..0f507e53 --- /dev/null +++ b/build-projects/android/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-12 diff --git a/build-projects/android/res/drawable-hdpi/ic_launcher.png b/build-projects/android/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9ed04d0faba356c248afb6336d6d79d6139c69c7 GIT binary patch literal 3041 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 literal 0 HcmV?d00001 diff --git a/build-projects/android/res/drawable-ldpi/ic_launcher.png b/build-projects/android/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bf733ec582ae2a7d2474a87b473eaebc22e3359b GIT binary patch literal 1522 zcmVPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*%C z4ln@PvuBDQF~krN@rXxo9=8xG z5zguJc?>Qh9Hfo+*~7Gdx|~|>*~e#ZY?Q_bOu3Xt!&pb74eFc??h*z*gtypDA+v_f zH237(PsI^iWovvO1EC=X)VauK1{V-Mpql4d%xZ0$^@%99nWwypyvv4Rh+!Cp9cZB} zj1OcTH8Fq&g9&F5zTqxwSZ7VGqF_D;HbDNR{4@Rn{{j&~sexggbdpJCVp&CJt_9kM z7zaWbil52g3|+3Mvfcp2*NVUM7x@iWY<%Mh`Cg z&j3HJ&8Ii_6-_T%2`DEFz<%~8XFdyRIxx}!W&K|UwCT|<(pUi=01g5#LCMXMHgAqM zEtWD?R8f_DMOZ0j0v7>8!?=M1K+rZO#C&|zn+g@;E(w%$cyqj&LVTDSYUrXX^->k! zN!#R37*oyX%q82*;?w@qcfAj37*yQ6g%l1CMrl-{e$%s-)q1estQEt`w|X6r zN;kz3nBCAU?~ogSDNG=jry@6qDfzBzyO>lki6b1LTB_kU;7j0p;0DlbgBi+O9S%QP z(X0JqHb=uu^$PjS!ylDgY)?=F+_4!`rO=05Kx3SHL<5Zmf_!Uk9e21%4`D{)j=CWT zGywreRiv_gbPsX8Q}1_frkksNlvb)}iBiXsdW5Z+4JYm;0euT_Gh;xJY!r{PBw|dZ zn^=df^@sG5sT8-4_94dzE);J12dDlJVU(@g-nIdVtunfjgt8QE8e+VeyMaEYxEzje zirg4>s9~K)18FJvCzu4X;vIbCl1ASE&~l)P%E*%4NsuNxg=OoAJJ!@ico`pWu-xqM z10MPv$fS*FtijOER|(z(@7GHw3CcOF4Xt8=^aA-PPEn)hkC|#ag2&cx5{03wLnWxL z*oHojk2%SGmHvY*1OOK@=v5+~M;_2%v95K+bj+*xm=o_ie{>_fLOCw>(9J!15QN7_ zPq0s=X`qlpDMMGJil7LJ7x3{lEA(EN;59sc%p`+6US}*X@*p!v2hz+J%t1N_Y#XR( zb=vfXEz|=ADBAHQZm-GJiv5X}vW`dC!#Kita7Qv&ezLL#5JD*Z&bS%~$5SPBpjcqb z7(*5-Nn<~c0vQPXD4Mlm4FGsK%15bgeZvdgH_Tro7*8In$Y3R-SV9ZU1X#*3E~lFI YZ%}7*8|5dOM*si-07*qoM6N<$g2ITQL;wH) literal 0 HcmV?d00001 diff --git a/build-projects/android/res/drawable-mdpi/ic_launcher.png b/build-projects/android/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..560306ac8c94f60177f43a6ea30898dfaa578340 GIT binary patch literal 2349 zcmV+|3DWk7P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*%C z4lp$P2a_uR00^x~L_t(&-p!eNY}3~j$3NGP#33&NBrL3Kj6z!oEvgv&vrdK4k@3)I zH7cedZIdc7u~8^Uq}sGq>&B>>Nffbx1{>W_*(%l|D6_$V2^bSYffPbYQ%ZRa7=%EG zlQ<72@$c*pUpv>Y_!pe5y^(z_`~L1d-*dj_aZg}4#w(GjbVcbT>bZ*-VFPFQITd_^ z0zd(4z6k7;FH^NygP9dDc;$7~uob7yxDF7`0w1s~|&wpe3k=sYQM8w)*?GUFpg$M-U z2Eu9mdLN7T5q2T`ly`ZJQf87D+i%gQL7QK*cGs0%A|ev9Lb6Pj3DLhOh$aI>one)? z5w_uD4L<-1{(r*%RnNbo+8^?Z9#*q7OSzN}07wO*&Y0^EY7tJ*#~-=I5?+s;@xP+i zl0!=(A9l0W-{%*x#InQ^5s`CpE?)RV5H}JmQD*=;gYYke-yy8yRjMdv5;?=?^aelY z*UVpA6R3%(T?;QFvO-p*1~3?%>JqF{kHtR`-lB!Ayu? z%5&#|ASjfcP?@$8ecB!1=AdgL@BKgzJ)EI|1MK1NRLAJGec%y?odm@Kie~Je@y^kT zqpLhFk7(#BI@MB}8GaPs7{H^2O}!@a?g9$PD&agf+&9QfR3xO;4XxR0< zp2&CP10)F#P)|KwblLZ$=oU`kVPHfWI+=h0fMF_pQjmzFACtBz;@Vj`&nZq3B}&-{ z4e#;Z@YE>E08s&)OrlNVT0!Usa7^AldG*%sY~41cXiBjgHz;fg4^XX>V^*3sKZk%% zfa8$Cu=$Ln0>D-BOv$FjUsxQi2y72O{^8MlA8-#i4}7d6QWM|?@{GI@KU}ypg-#th z#q_UES7)c5B}Z})We>?Z);*ZU&81VNR5~kdCdI(e0a!pM(5QKSM?0w*kwgOlXa>sDSkA>X=91$ez?<{}buiyQpBL}G z7~4qFq)0DbT4E>L;CBeP4jcfs0K0*Yj0oe5+bkImYLHgT2up)yh=9jR#MHqeN={OYQ6r?Pqk^Xlk4_ zH1s#X4d4{8S2wtBXa}^6XVnn$L3j`VQGxsQZO)
nUpj#$NQ9%n4!6lp+=>tHEx zNyqRmL(?kDq}HXanJEkx0wc9dciH5KwJh#&lKwO~Oc@vQ!&>95hQnnmylCr*4NCi}RqAc$U@!b?n|b-=TDxz3`* zYWq4NCNhe#JQFJ`0vd!GWMdbQtNmms9am+P)0wocXaiQG_ze)|B5*PJ#tC=%kY2j* zFd{DUWV+Ah0QniRY2xV1=s%*1%#RS(;!C~;CU@p)@N%6N97g1^qe-PN<+(#}>YhRO zkM^n$0kiSpPu0*(-JR~BjeZ@fh8k5$4XfD6zNsk_ZD3X7qY|%fyn5{dXyjX5pqEZm zTs3JY)ms`wrtlgq&QiCWE(0s@@p7g%dh(TdA#jWq%6W?bw`k|G zcMq~gj8>wGqf9UG6?`0Gp^lC=ntOwfxy+)FHIy)ik>oRi0vv!N4U}llvrUKKR426> zMcF{Cslm(qm?C}lyOZgMxzThADPtyMc#b@#k;iQEb&K58tl7Bj&@c0QC-5J@G8CX# ziI*)bO3n4`ERaegvnimIafDe+9@UiTTHT>_DGA!{F;43OT4SxWtH6)&axk0zaQ0&k zvnXN>V>Qj(6yVgX78Li4lA!~k9fjgUgfHWxXQ-MVUI3U^`Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*%C z4ly+19HMFf01)CyL_t(|+U=eDb5!Sf$3O2`NnoSZjo_hROoUsfLyB*-acv`Lv6<39 zoYH18wOhM0Es{uPz~i_r7dIhIi)l0N*ti{INz+c#KcJWKie_-iMS#uCjlm|4FIWwU@JJ&)Hq4ei_Yz*gfZcpYQX0pUeBaXQ5I@n|}Q^FK_~( z3*jY%#}NLGCcZ)#fc1Pnd>=dY>qa_wA3-n~K=@yTS25{A*v#v!<=M}L<@flW^`bVb z!}YOx*(p0sOawvbLpX(S8k5%%{t@9je8|I$QcoQK>pmBu_tBwWze`8$M6HM^vRO8Z zwgKz)=Ng1l+9FON{2M0!fUtq9w9^Qv&*{ml@c)U9FZX>}M8rHV=LP2F~V;Le#qgEwtTd5_N>`bCv~b{_47r(nEGDPvmO9G za;(7qy@@b|q6d?A=x3M)_R};CM5M9+s4DB-ZGp4{|3b$nkA1STv8EA)QYaW;AiK)c zWcj^;q6S!t!E&0pjBr1~TkPdJyESwb1BNQM@Qr(oE#|+POO7r%`s9Oi9u()CRJQ5a z@45n<2dcBY$Iw3(Z7GfcakkULL4L}KN&up)6lDCG(Q586J4e4cN<*kYL_{g25CoY3Ps{%=3ups6fp5^lWAsEfM5R`0l~SowX{3>> zTy=NND?on%tEmCr0X_vz0ykK}d3rg-#gZ#wIfQRKY_!rw&)E%UB`o0-;R^yl0}bx3 zDZo5nF)-zF9yfqr0qz3+2w@FJS;_#9vNHw7^}m1?V5~M;&An!4^p2>6C9Fir4650~ z9-iWKNbnQ=`@Vs@Zw=ev5Xp z*yi4iaI%v`@OuvJ5O?qsM`HJ0IVos8gx(H?6Hg@idj7Jf zv1LvRZiJsV`z?k>#(|&tsBS*io&2L9y@M4x7^+QtEfMY5*3sB9r$qs-g+InvFdnXE zx+LN6b)9vmKc<-{XkaJ8@ADESi`?WBzFI)_z17R6{%q=O$G1DAjW#7BxD~#geEAN7 zJtnjj1QB+)sXJw{2!E#23Zavs+Qid|*rCS{u}W5vE7Hm2d=-jjn(3o27y&6Dp-T@n zB}i{m>rPWd_=5iwq_+=4vqH6rKTO0r9`BGxp5^EB%j%^*%CT&WTx0N;0Sen912v}Nz+{YHf3E%|6 zSLyfH2fPfSe-70q{wfhW^hn{f;~YZ_v4};5l^-gZjznMgxXygDT z_5*6?@0cIk`^a8TwBsCc;-wLOspv>);lJ*DIHIEuDynDn|2d2fUcY zlw}BTGjIc) z9!h%~a@6pyEKGIGIp_V79NsTyW0=JK_7+EI?6GzS2G4LnPur_p#LQ!tZ$~P7P7m@(9@xs#frETZ#Jtux8SoySHRmib~tb$j- zb*#PQBfzNsnbdS-dfJhGS)cNVETYq@#ljrKs2?gxk7OVX$SW&r@^FDA5IHUjF zr-8VJ)oWCR)otO;r_8K|4!%3cCN`-HTmT`^#K^w(AcK{m?dYs`QVaJ8Z~!HLdjj1DihFgql>0Gad*fRh8;knxy^R#M**9 z0Q|^WD<1$KYML2JvyRlPab_}hLsL_s!Vdv!lNlIe_!p3ML%Xl5NDuTp_)Y0qorAKg>rkk{k4jHQdT=46xi` zb^_Ewczu|v`YJ|b#1~2`y7Re^`2B6*lGf}5a8ZMia0t`wa$-OgdP-A@_&Q4D?+!ViaushCAcMF0_K^fRSb(61fnDXrb(THqgRVHH}xiEF9} zWf7(gsF9`st|?g(USNU4DZf(451RYJg?Xvb=>J=_v?lSI15Z z75>MXQYLjMIjlK(*rAzFT(weZ>dKe-3Rd4+*yT0*>E*{PVZH5o6k}Y+bnM>G9Fta@ zeB_NIC{tM=xQvn%%6sRV^YRHTjKjbo9ntjYXr|v0$(R&hDt0Dn%7sv`I-GJoN4$_` z*0P%j7_0+c!>Y%h)gffBwgeM@ z&->Wt3fzJiawKk&wJcsYUOk6v+R=vazwO*UXI98{-GY$fguy<|(Zo#%>&V>|pd2V3fYG5* z>hG>68jcc{aK;IbGMbyJoKAI0Q;F?F-_{l|sb7^t(}SuVibo2w6SNU<+w5m8C6baY zxgQCwk^8K~?fdHc(~tIXj;wpX&dl?=7H~IkP}gNnYHc3|K5^7#N>LTPLE7$ZftKU4 z$txA(4G6!5FuOqPMMnU4L+zJpi5ao!P&IGobGGtt%mH$c+MYdaFPwtX!`FpboOm1< zWE=bW`{aME14d|IIioD|c+)B1OAvl2OrOz50}Tu@#BJP`+7_PB8uq5<`0HBBL;BUA zyw7nF4Zt|Xhp`JXhOt5v#Td=W%2+q>J2Wr>BoIOc-dgD>={<-gV$5AWmo4+QVAz~t zmw#=3k2~ryamQjqlz${N0DjD6l1zuir(0A~h5R6jI-X}OPco}W^bL!W_;s!2@9MH| zG)wunQg$ir`CRnD-`K1>KLePS92Te6hL0hvVU$6v$GH?!1T}TMK8Dp%@Gj6ua(j;w zL^EC$=Ymh#ue2QEj?h$P3PVW?TB;l$Rz;wHlaAp(#oEjsc2W8*xAyy;Qy7|No5;AA ztTKE+v7Q0$XCZSbu3!F&BR#+DF{~@)aL(Cps|*3Z!(K4r?+$XeCSJbKNBXqCp6e?9C~s10E_#9m?)z*bY3yVS017sa4VF`l*BI zk&oLn{brqyUnvs^QljiU*_XeP2uEyRoBg)dUh#HL((5_$aRUO5^uHW*WJe`Npb&#% z0N6AweC=zq-`3tMuEt3DqJ%;tAGc}xyfRWyu)5#ZA11%9<4y68Yq#B6|l(N<>F?KTq6OQ00T|SEAt^fYaT}g42sknl2Zj1G|0pm@gt|hW zBW<{xtldGaQjYKx6Zzx=A)%debwD6^>3qu#WeXAJ7WzbWcd^TPCYjFy!u&Q(deWBz z(Ed!^Jcr~SvftPLxCjMGCRPd|Og96p%kFH8kAM|b(}g)dBA-I^%kHhIl-f^H`0Yec zD8^HAr$7J@_HmhL%Bho5)oDzi)<+OjQS@mEUuYvjXR%-6;BE@*fS+)JOT@W`XRR61v&VEx-gT89@;T41_p}*#QFKq@p-h%b(YK#Bg6WPp#+tL80G}5nLYl zyDUIZ)bI@OEkeLTganHD0hSYNag6EcNO=Sx>gu!h-8zV{$xDR)rb@nOV)~Uvjt0*T%M4xI8=nLI~VeGZ+#$=RwcB3$;$9h(dLQzZV zK{~HiU}z9V3LsKwXhLobKSC zDiVA!Stw0;p{NN>M{cZUAa4j72F6SE9mC~o49`SUbqMvS8BZs16P{jl@tJF6dcuhl z@;iy}Rw>0$9srzHkxo-Jz%2;#v>SALfWy=vgmSF(Qd;bvbhaxHdgg))6cM@gpw3~8xgw7we$*oe4&G;;*8SBZbBG*0hpDC zrlz!0PG8w1_AZN^5;o7mby!=agCSpFIEyrKvg;mo@d6zz`>>;s6nLREqFF`vAPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*%C z4l)gvGA?4aFLMP`*~8a> zKLZkI2Hpkg`981k*nsa>0bVYQ2*W)p?ouH5K9cKL$|re=^?(KR07AW<0vu-@ALkxE zH}stfRlW@<NY`gvU-m8cJ;m1&w0yeE&3W@w|(kzvy}YL^;u# zEG83JdZBEf6F?V`1GoS`l6nNq%2w(I^6+w+8jeuIn^XtvBSx={H1ceqr-3`|m)19| zrM_!?PgPu3F@D?BZT|UkzO`Pg4}EkbP@;aHR3MV-6O(~yKr!IM&v~lIql$dqp^{wQ z#fO&{1Ls}_3RV{+P_~1IEmV@ve^33&RL{Sa{hPc}UMaSUt=!3-2n5p9KGKjx$ADfX zg=E-c<_4PZa}L3Wv!@kJd#inAJG(r)&SG18LA1$?LsYyEin)bD zWfBi^gk8YW3pS8bhv`bc%Js9$X20}57ypl^bWEuPkWvaE>|6XXKV}{4@CEeIFu-UB z-dB&97~*rnn8`lny$J+$zybPbqK#ZG^LKcu6`^m#xX8>nXExH z4w!{aH2M{ghWj3Y&kBlwLL_4moaeK+fdy>i2q!p8F@9dc%P+H;Ps%O;tp&C!j^`8B z8s8eYD^b-yq zJ(jba7kP0Y{}Zi|EYyP685~h8!{n3P>R9dg=~aBv0s1N9YrKtz-8`F3UYk4X!1EQB zFpupO>nr$u&hOA# z9#yZu$`WRl`Q(*^Ei-eS0m z|4D`?8)MZ2Qc(b4Gu!1z1Qzq~FcA+6Im$i?IL_5nQ=4fn$>m7!JdNOfr0Z{8_xhLl zH&-sJ%zJsv%W{I8Af%8W7*cf!^=Bn3d4*T#r$03y%2hyWG*nQsj0&k#q#Jr^ zjPZ=8iKf(Ct7$|x6CK%@=()7b)rzDY!3RJi2Wa9Aj!=k;Q@q0c>9moAqrjunF&<^j z?Vq{*$+FAK3Re`ZkXOhnY*#(T)tSKzj&nS9*KC5Qk=V$_c+aQxh8t+X&soJK9R_mn z?X@S1 zY>el;8c64XGwRmsfwzGI)^V7#9AYPjs7n=)G7F&a8+wf;mo;9t^NqiHgE2W{2E?_Z z9e(`mWiM4!amJueQof)XMDdV~iJaF8p%f$^0u2b>0eabjkKb^ZR`zgY$obDI0I7?2 z&35JcCizPECuV*k=M!%no_`o1Ztwtf(Zv|Xr2n2Yh#E+XCh@Z(m_4_?1|r2%FOL)Y+S*5KE>vRcQ2&o;+mu#-h(Ie zcAazx-PZ?<$Ib?r56r+$QD~%&M)Jtza%F{P2@kXe_%&Fwt&MVxd}PV`CA?DligZ~n zp%TI3R$T(YDNZqmIf=)=5`8%Gfw91u46$t@L=xG4g$I~~WHN%8xETXX;!BkC5XH2x zTLGymdLAi#Dr~K~nQdTQZ$07vhX3y6H!aV7ruZ3IMwyUO5-)Jfag?LXWM<~2k0ui;P3mm3Us^X<8~v;O_uaDemfRl| z{YXxe(_$tFhq8a#XrqWCXQ(ifBl?Ks1z(fNCs{xTk~Rd#fWw@ppWm_{iR4@SiN4HA zATu4~uUOA5Y+nA?%lS#cPlCBTN%D73af-)yjHh_Y5%g9Fi0ZM4Mwuq}K2{pQ4Rj$n zgWxQ7S<{cy)eqsNrB*we=@|F1nk)I2C0}2{vw6?TNpezB!2__Dz1+h+&RHBYZdRaP zcM@8(=!9zoJJ~GHKQ;H)qQlYKXd0G)C`BB2yo>NQOsrky@-e(oZxFXYe-wdx4{fK%SEvx)#bxC3yHQ ztNA#;n!RSWtd+GQ72wh1U-?&7vJyuRVMwBBL|*}$YVc@4*8oR}oFum_T>#S9*vxvq zz_!Y7R#L0-o>Dnn4oz(ubW8$fLNZg8;u>!3tjD+1Z}$L@^`=h(>6DDe*m&n>?%aHK z=~=vZ0}T#RC4I7Lm<5Ovu#>-fGaz{+HnDhANuIh0G($Pyp_Ut2IG7if92=-cUC`yg zBRsM03+wLQ@Tm=yS!FVnY72sur-c+|ABc3};%w4KHtO@b6*sk4**+${V1Gs`bQHnh zk?F($q*+Rx%kw%L*A=e2d)>wBa>wV54@A^cDRnF?ym=^B(UqmE++M4qiORT zGr$v`Nh#Fd(s)jjuEG>}*0Byg$D_Q;Lu;n5*|1^3hTO>olM_nEh;_Wq>+EA+_Nm8k zbvAvtZP+4P^`#+^3sBwTswG)Xi` zaWM_*x0Ph@_zhS66v0F^(g`3{W?Vplr~R+n@8%LVEm^xnzBuJY0Fh3~BM{V6pB67K zZIWoJTIj|kfhZDqjwR3Da>oN6fcBf)?_(jGuKoLK8ejzK|oO z=eJ2VPJw88Vhw9BnmOtstawoWDB zb4!)dAqN+ngAb$YyZEAbPTnnVTYbZ7wpv@IkV0gV9K4lV9RcE$ayIobN)Elq9)stp zk9xHugSd%qx>&^yqp5mS63EEq5qF8_150tALHGm!tf`_}=DsPdu-E-AFJmPso7RUnOgs_AolIK%x3t5UI@(<%Z zMN1OKZq$djTaN-jW|{FiHnEg7tFKwjMr&in!IQGi8%_t&WVM8+8OT=a&qzW^!Zc(* zx(5Z%EqJ(=`mAkN^Ai|1Px-bOLzH#tEgAjvfG+AOiTfGLp}cQj#z_02z56{!;(Xna!^yeU&15ELuJY6=<1qFviVbO!JTh{9n7yaO3h`g^i*nIF-tdonZ_4*1c2G3!ajj@Z z7hM!lMBmW$ZX;Ln98mv$AG<{8m;#WC)aDU2(@#6=*kN?k2i$y_*W?58nk}=p@OOoO z=UDJ)`zN1#T)0LyfWmhH@7?R%y47z@8lETEAF&U+qC;1ewh=6$PE*mMn}Z;v;PJVSLm8|XyMmv)f6jP0-Z3B& z2)gKEH@im;aQX}tw4WgPSF7G@QBtN?0mCvRm1ddGtIxQMWA`X&CDH>TH_aeFl5hmX zG{J8d{+8Amt$2kOkMsmCz$Ec9G4`;BRjkTx4@wYhzIjrqrY>~bIeVN@QmIwFXAwDK z8L8io4s%Szt}aajh^)W~@<7%}*b6(4?&NB&20@_DppcnO8O^G|sSGhdZ9wScu!GpF z@qXZ4_3yI^IF0J3K}nLqTyr^u!HfESUy_Y!b5xn@@(yCPp2_4iVFVbTPUKOhQi9c9@@ zoBS_HUriq^3tE^mX9|bKVG3no+U+8(ItwVsE|02Bx!iq=NaVu(yJGTS=YM>Zd`u;dROY)-JdR`-x`w7`joF z=8}30BRf$tQD7mM8fsd+%$qEwizdY(>zoMU@SH7%cc0k}Q{d2)LwNE$Tqmz19gP!;Bd6nLRz>MF3aKOw*m0>2 zwZNTf-cZ1a7c`O7QybBREi9$xbj#rsggUfqGgfu%9H=dIl6IplqQwNk{!91s?TT;n z3(qgON?w&RSKq?oFo$`Gmoo272?dHC^$hB!nE)TE&z?0@Mh|uvA{Ci|ZA4$SYAG(M zTk%6l?$4+Jw3{w&riXm;204k^vRXDv#C}1otYzigmAvSFkxn}KET6Sq{Ul4IZEWM0 z{4#x&r;)-7nymX!JxF?gFzw;!{ajiUxJ20OCR=Ul6jq`HkZ$Z*LeiTW+K1c0Dz4*I ziufGG!ywdSgNS`_Tps7LS(kwe4tNeQN6fK}Y>~J$3irNJ4jyxXWEhX~o)Z5bDaWFR z(C|W&@%>cQIqK{3#bL{6iFgjXOvs-AMfYaMKF3At?yy4Kr}$C|&;s@Fs8> z%)N9jweDJGiCIV_F6l~08tL!#7@HKQ1vo-^NbeiQ>bnY1&B~tXG5};g>FF2^B(~+a zOwhfz6lS~wZ#yUEeLr35iFn60vM=qs6c;Xj$M2FdS9fp+8`+pXo2S91E!c<66z^BB z>y(;0qimlx$U0tCC|l@E+>*!cRaU}n+{7*hXz30XbBtdzmgRg>iM-+1PJQ5h8$>-; z59Gkg=__d-*UVbEHX&C(CEGPciccL=c7l#i^(!#x%dR(gmt+ugiP8p%s6g!iOpNh( zy6v6)tua9WRjD_t>ER11)worCM8UEA% z0eYvTp97lPc(>*CW*lF*djb}U&A7p3|?y=ZRNt1kPDk!o7iB5sx%FxR$A$#b8yg@Pf@?&9i3Au zN+`(#!SI5m0Ielcwlp@=s`YG&u359%{+R5c0GSvGKs_ppP7GMEM0NKEY7$)V;3poW zWkL&^#pZxY(_;>Es1KAM5IMY{&HQ7^AZ$_|mChzOV;GAjC6P@XVItjkDgdOVa#56P zDHN2wgI(4VZf6SrkHsV}HBuTE+>hNL;M1^r!fLk3Z6U{>!3>Ubd~ogtY^K#oaG=dGbi+ts=_$D#w5CpCI=&w7ql0i zJM$b<#Z<20n!t?&>>pG5bz_GVKEN*T-LLGNqXZMR^pEb$CBuyCY}@BW2SU%L_}WDn zVC67mqW$@?0|mM_aKQX9MUi|Dzz-aUF409-MHi>7(=3{?2q5Ub>(tN&#WB4{Fg|s{ zAlEvKtIbwvGn91jrNL&?59v82g(6kBMj#U-TLgYyrk5=o;Q_8B7y%YZe!6_vX`n4Y zF1+r3-R_TEr`|iO-tSa5cfxRKb$CJ7S2>ltagzHUjknjKUXP2{3$w%YbzIAHzyq{l zhc!}hBQE8hcBw1btBl}x)&Cz*E_jgwkf~BT4EZ<`3r4g==uG4NnII}QB)W(NKw6FE zQ-~lK7fYp{HycF0j-8mUi9aoU`ji?oy^;~!V?01RbfnpH)brbcoeDdQYwsRHgjz~U z$dLV`QXz@V7PWs?Ze_l415H`R-6Uf^$dGi^Qm6^LuFNy^nh$k(oj@zy2>Mb89te#R z=U{Jp)TfN+P6hMPRM)7Z@BR#)-;5Iy!821~gUHKL1?*%m=b6KPWz_mp{IvND?#2g< zSB|1&^bH*ZKA+(S$BOWf2j8=}3CkMh0G_n*C*kLjqZB+&qaYdx&B~Cr$4ogFVbLBL zKtVpp3V2JYT25VdawY=Wi;Nvs0?Kp!u&h9lI|lB9)`#zaNqs+w$Q$p|-Edq{fydeVljaMek9 z{^6F+tk}rG6{yTihV=0f+kRH2aqS9wYDdyIOYFrA=?A*h5;8WL$}*NsCLn#{CCbcg zAl)MfL^5oSovfNIB)tgEAc3w}md_I+36jMz#5>^=3FcdmBsG>T!%qXZGB$bu`Iy5I zYB-EYD8)ec_lrjrYFkKyrpz|vvfjPd_f_xcdkOaR5F9`7_>L#^8puMX5@wh<*9Zc6rHg zQB96$9Kj^BY>V2PsVvTwJQkAY5v<3nN;8+)*of_!y_jY!KFL@nQcU!PXyzoHBl`Ai z(TaXI!A!brY`fxD4n?nHY@zUiqTd4dQ_V@1Avla(uo;ukUxr`@?@`L7jA0=YVz^Pg z&|MJd*pl4sh=LvAokqY#tGdqOl**AHcqG3lr0 zL`f9(kcs7{6it#&F(^^z=+8p0#u<{+T&~xpXO=?OzTrDqRp7uL_&)Toy_WH9_YcO z8vE5wyj*X1-$Rbr9p5AT2NTJmgGGT3=+P`}N1C2C4brg$JHtB}K#`K^G6&@ssOC!m zRH*+sH1ro^lR(RqDD%-1Ra%9v>uyTpG?+rRT?&#`)20;q1R31b$34 zy)0AQ&Y{>27dwEe5d4vIlw$!a1LZeFkUl-w>8hCuF_?z6&ta-emoB%ZZGLQ-7wjpx znQ9*4Fus6k632+$3)5Uc4#l_t_aGaz^g2XsNW;)?bBH9;piW=qsa~UYU^@~&)jSwt z*OD#(1;+^_M``0@fnJJcz*J`&x*hPO`v{gvH>X);40cWZgzdbQa;4RS(N2MIFnjCA61JJlTfn0#U9qsx2~GxTs}8E2-={7T(+aM^tmaxnXXi;E5kV z!44kwRyqw~cD)nIaw9F8Ur9tWj+br_g*ORJ6)_-`)Un3~ynHVi5+^wT8jOZY?ZhN+ zxUkb=MFzo~9-uD6i$L;3s!roh%J+2(7=ajcH({qpq44dUGU2{A6YK<*8W$Y|i}3O~ z0M*LV@}}l?;vfC&!N*G8M{owcowIYxQo^79@rG(k=kTS(kPmij7c32cXPJ7pI(5%O ze4`3@m2S@Sr@$t#EyzHwqLU-xS_f?gypL_j$vxEoALRhh)D>@aJ;MT&vz!VdB#r}oQ!Tm?szo#UmN zUj!cT>hw${2}vQXSC_JtGS&oCLm?8M>S>-kz(;Tnp!A}45Zq2XmENNcU-Tq=9zBaE zfc1EJKCmW9a?w*OfwUB=QosMgOI*Y}x@f^q0iBEqFu(ao`VoPoG7(`KGVR{y66{r! zDjQDsd+bdDcx<=jEP{LR@&Z)|4ym;`l-}t}d<1trw8s~D99WB_nOw$DN_jx?OagL@ zot~^gIFd|BqAu*j|6~H&%noQIz7WsAy|H?cR8h@NwN;nQ)bp3z5l&VuqY`)v=t2VJ zJO2enFWTJz-w^Uq51Bq6R_<_mFV(WJrD?-Yp5x`AM0dnZ(9ECg% z+!e^%nWEx15!!Nr-K51hF}sBJ7H}ietfE=s3^s>fO$N<%b`pVM6K8S zHM`U8qB|Qv;nFFb^tzG11?J$Vg)v-9E>(fapN3t8BGtxJIfl)WEEn92nQ4wF-r6<^ zoQ`_N^{!m6BDfbX`<*6roClzRfC??4y^%{1{18bS0yo9n7T|9NBQ295nal*aHU*?u zY#8(Wr-!*78L}tI9^e~PQ=3T_J$nHZs_F(#;7#lT#^9%$VlE|@QvBGxg38rWgg2i` zR0X-QNx*huqhGZ_-Zbbng#T|XuoN#RoerMt0MKw=-C(5UM5Ws7eTPKo5JZds00HYs zL_t)-?6=sQ9JH6(Z>a12QH2=mXGjfQm#~S_n$e8@K4ngrQ}? zV3I_uvep?$=)ld#$mIp3l1OFP0p78Qkllg&5oXD>TFkxb zBh6$VcKT{x;5cS$!0o_YcxlQel{Gp5G@NuYNy|PuC{m=QTdAl*Rr6m==#G6Rc=XJ5 zDw`-=a_P#`GuS!f1YGpZ2;Ku~@NzJl!83XQG@Po_q-DP%E%T_t!rt7aNWrII2vrwO z5^D9;3p6M%&>mZn+)p*O<0w z2<$X1*^Xc(USzgQoeKaOTsjR-TK3aTi6Sj^KgJv-hjNuy))jE~CFc<|(T$t8fL^>T z$u<}L0)UdO>NIKTrx_3VjAs&HXXv;H_93!6vPqixHGfbNY_MZq0MMvWb)k}D8CL*% t5TIaWr=WBJCxJG8$qv3b5 + + + + diff --git a/build-projects/android/res/values/strings.xml b/build-projects/android/res/values/strings.xml new file mode 100644 index 00000000..21bbf318 --- /dev/null +++ b/build-projects/android/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Rocks \'n\' Diamonds + diff --git a/build-projects/android/src/org/artsoft/rocksndiamonds/RocksNDiamonds.java b/build-projects/android/src/org/artsoft/rocksndiamonds/RocksNDiamonds.java new file mode 100644 index 00000000..1415095b --- /dev/null +++ b/build-projects/android/src/org/artsoft/rocksndiamonds/RocksNDiamonds.java @@ -0,0 +1,6 @@ + +package org.artsoft.rocksndiamonds; + +import org.libsdl.app.SDLActivity; + +public class RocksNDiamonds extends SDLActivity { } diff --git a/build-projects/android/src/org/libsdl/app/SDLActivity.java b/build-projects/android/src/org/libsdl/app/SDLActivity.java new file mode 100644 index 00000000..18e59c1a --- /dev/null +++ b/build-projects/android/src/org/libsdl/app/SDLActivity.java @@ -0,0 +1,1743 @@ +package org.libsdl.app; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.lang.reflect.Method; + +import android.app.*; +import android.content.*; +import android.text.InputType; +import android.view.*; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.RelativeLayout; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.os.*; +import android.util.Log; +import android.util.SparseArray; +import android.graphics.*; +import android.graphics.drawable.Drawable; +import android.media.*; +import android.hardware.*; +import android.content.pm.ActivityInfo; + +/** + SDL Activity +*/ +public class SDLActivity extends Activity { + private static final String TAG = "SDL"; + + // Keep track of the paused state + public static boolean mIsPaused, mIsSurfaceReady, mHasFocus; + public static boolean mExitCalledFromJava; + + /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ + public static boolean mBrokenLibraries; + + // If we want to separate mouse and touch events. + // This is only toggled in native code when a hint is set! + public static boolean mSeparateMouseAndTouch; + + // Main components + protected static SDLActivity mSingleton; + protected static SDLSurface mSurface; + protected static View mTextEdit; + protected static ViewGroup mLayout; + protected static SDLJoystickHandler mJoystickHandler; + + // This is what SDL runs in. It invokes SDL_main(), eventually + protected static Thread mSDLThread; + + // Audio + protected static AudioTrack mAudioTrack; + protected static AudioRecord mAudioRecord; + + /** + * This method is called by SDL before loading the native shared libraries. + * It can be overridden to provide names of shared libraries to be loaded. + * The default implementation returns the defaults. It never returns null. + * An array returned by a new implementation must at least contain "SDL2". + * Also keep in mind that the order the libraries are loaded may matter. + * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). + */ + protected String[] getLibraries() { + return new String[] { + "SDL2", + "SDL2_image", + "smpeg2", + "SDL2_mixer", + "SDL2_net", + // "SDL2_ttf", + "main" + }; + } + + // Load the .so + public void loadLibraries() { + for (String lib : getLibraries()) { + System.loadLibrary(lib); + } + } + + /** + * This method is called by SDL before starting the native application thread. + * It can be overridden to provide the arguments after the application name. + * The default implementation returns an empty array. It never returns null. + * @return arguments for the native application. + */ + protected String[] getArguments() { + return new String[0]; + } + + public static void initialize() { + // The static nature of the singleton and Android quirkyness force us to initialize everything here + // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values + mSingleton = null; + mSurface = null; + mTextEdit = null; + mLayout = null; + mJoystickHandler = null; + mSDLThread = null; + mAudioTrack = null; + mAudioRecord = null; + mExitCalledFromJava = false; + mBrokenLibraries = false; + mIsPaused = false; + mIsSurfaceReady = false; + mHasFocus = true; + } + + // Setup + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.v(TAG, "Device: " + android.os.Build.DEVICE); + Log.v(TAG, "Model: " + android.os.Build.MODEL); + Log.v(TAG, "onCreate(): " + mSingleton); + super.onCreate(savedInstanceState); + + SDLActivity.initialize(); + // So we can call stuff from static callbacks + mSingleton = this; + + // Load shared libraries + String errorMsgBrokenLib = ""; + try { + loadLibraries(); + } catch(UnsatisfiedLinkError e) { + System.err.println(e.getMessage()); + mBrokenLibraries = true; + errorMsgBrokenLib = e.getMessage(); + } catch(Exception e) { + System.err.println(e.getMessage()); + mBrokenLibraries = true; + errorMsgBrokenLib = e.getMessage(); + } + + if (mBrokenLibraries) + { + AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); + dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." + + System.getProperty("line.separator") + + System.getProperty("line.separator") + + "Error: " + errorMsgBrokenLib); + dlgAlert.setTitle("SDL Error"); + dlgAlert.setPositiveButton("Exit", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog,int id) { + // if this button is clicked, close current activity + SDLActivity.mSingleton.finish(); + } + }); + dlgAlert.setCancelable(false); + dlgAlert.create().show(); + + return; + } + + // Set up the surface + mSurface = new SDLSurface(getApplication()); + + if(Build.VERSION.SDK_INT >= 12) { + mJoystickHandler = new SDLJoystickHandler_API12(); + } + else { + mJoystickHandler = new SDLJoystickHandler(); + } + + mLayout = new RelativeLayout(this); + mLayout.addView(mSurface); + + setContentView(mLayout); + + // Get filename from "Open with" of another application + Intent intent = getIntent(); + + if (intent != null && intent.getData() != null) { + String filename = intent.getData().getPath(); + if (filename != null) { + Log.v(TAG, "Got filename: " + filename); + SDLActivity.onNativeDropFile(filename); + } + } + } + + // Events + @Override + protected void onPause() { + Log.v(TAG, "onPause()"); + super.onPause(); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handlePause(); + } + + @Override + protected void onResume() { + Log.v(TAG, "onResume()"); + super.onResume(); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handleResume(); + } + + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.mHasFocus = hasFocus; + if (hasFocus) { + SDLActivity.handleResume(); + } + } + + @Override + public void onLowMemory() { + Log.v(TAG, "onLowMemory()"); + super.onLowMemory(); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.nativeLowMemory(); + } + + @Override + protected void onDestroy() { + Log.v(TAG, "onDestroy()"); + + if (SDLActivity.mBrokenLibraries) { + super.onDestroy(); + // Reset everything in case the user re opens the app + SDLActivity.initialize(); + return; + } + + // Send a quit message to the application + SDLActivity.mExitCalledFromJava = true; + SDLActivity.nativeQuit(); + + // Now wait for the SDL thread to quit + if (SDLActivity.mSDLThread != null) { + try { + SDLActivity.mSDLThread.join(); + } catch(Exception e) { + Log.v(TAG, "Problem stopping thread: " + e); + } + SDLActivity.mSDLThread = null; + + //Log.v(TAG, "Finished waiting for SDL thread"); + } + + super.onDestroy(); + // Reset everything in case the user re opens the app + SDLActivity.initialize(); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + + if (SDLActivity.mBrokenLibraries) { + return false; + } + + int keyCode = event.getKeyCode(); + // Ignore certain special keys so they're handled by Android + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || + keyCode == KeyEvent.KEYCODE_VOLUME_UP || + keyCode == KeyEvent.KEYCODE_CAMERA || + keyCode == 168 || /* API 11: KeyEvent.KEYCODE_ZOOM_IN */ + keyCode == 169 /* API 11: KeyEvent.KEYCODE_ZOOM_OUT */ + ) { + return false; + } + return super.dispatchKeyEvent(event); + } + + /** Called by onPause or surfaceDestroyed. Even if surfaceDestroyed + * is the first to be called, mIsSurfaceReady should still be set + * to 'true' during the call to onPause (in a usual scenario). + */ + public static void handlePause() { + if (!SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady) { + SDLActivity.mIsPaused = true; + SDLActivity.nativePause(); + mSurface.handlePause(); + } + } + + /** Called by onResume or surfaceCreated. An actual resume should be done only when the surface is ready. + * Note: Some Android variants may send multiple surfaceChanged events, so we don't need to resume + * every time we get one of those events, only if it comes after surfaceDestroyed + */ + public static void handleResume() { + if (SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady && SDLActivity.mHasFocus) { + SDLActivity.mIsPaused = false; + SDLActivity.nativeResume(); + mSurface.handleResume(); + } + } + + /* The native thread has finished */ + public static void handleNativeExit() { + SDLActivity.mSDLThread = null; + mSingleton.finish(); + } + + + // Messages from the SDLMain thread + static final int COMMAND_CHANGE_TITLE = 1; + static final int COMMAND_UNUSED = 2; + static final int COMMAND_TEXTEDIT_HIDE = 3; + static final int COMMAND_SET_KEEP_SCREEN_ON = 5; + + protected static final int COMMAND_USER = 0x8000; + + /** + * This method is called by SDL if SDL did not handle a message itself. + * This happens if a received message contains an unsupported command. + * Method can be overwritten to handle Messages in a different class. + * @param command the command of the message. + * @param param the parameter of the message. May be null. + * @return if the message was handled in overridden method. + */ + protected boolean onUnhandledMessage(int command, Object param) { + return false; + } + + /** + * A Handler class for Messages from native SDL applications. + * It uses current Activities as target (e.g. for the title). + * static to prevent implicit references to enclosing object. + */ + protected static class SDLCommandHandler extends Handler { + @Override + public void handleMessage(Message msg) { + Context context = getContext(); + if (context == null) { + Log.e(TAG, "error handling message, getContext() returned null"); + return; + } + switch (msg.arg1) { + case COMMAND_CHANGE_TITLE: + if (context instanceof Activity) { + ((Activity) context).setTitle((String)msg.obj); + } else { + Log.e(TAG, "error handling message, getContext() returned no Activity"); + } + break; + case COMMAND_TEXTEDIT_HIDE: + if (mTextEdit != null) { + // Note: On some devices setting view to GONE creates a flicker in landscape. + // Setting the View's sizes to 0 is similar to GONE but without the flicker. + // The sizes will be set to useful values when the keyboard is shown again. + mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); + + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); + } + break; + case COMMAND_SET_KEEP_SCREEN_ON: + { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + break; + } + default: + if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { + Log.e(TAG, "error handling message, command is " + msg.arg1); + } + } + } + } + + // Handler for the messages + Handler commandHandler = new SDLCommandHandler(); + + // Send a message from the SDLMain thread + boolean sendCommand(int command, Object data) { + Message msg = commandHandler.obtainMessage(); + msg.arg1 = command; + msg.obj = data; + return commandHandler.sendMessage(msg); + } + + // C functions we call + public static native int nativeInit(Object arguments); + public static native void nativeLowMemory(); + public static native void nativeQuit(); + public static native void nativePause(); + public static native void nativeResume(); + public static native void onNativeDropFile(String filename); + public static native void onNativeResize(int x, int y, int format, float rate); + public static native int onNativePadDown(int device_id, int keycode); + public static native int onNativePadUp(int device_id, int keycode); + public static native void onNativeJoy(int device_id, int axis, + float value); + public static native void onNativeHat(int device_id, int hat_id, + int x, int y); + public static native void onNativeKeyDown(int keycode); + public static native void onNativeKeyUp(int keycode); + public static native void onNativeKeyboardFocusLost(); + public static native void onNativeMouse(int button, int action, float x, float y); + public static native void onNativeTouch(int touchDevId, int pointerFingerId, + int action, float x, + float y, float p); + public static native void onNativeAccel(float x, float y, float z); + public static native void onNativeSurfaceChanged(); + public static native void onNativeSurfaceDestroyed(); + public static native int nativeAddJoystick(int device_id, String name, + int is_accelerometer, int nbuttons, + int naxes, int nhats, int nballs); + public static native int nativeRemoveJoystick(int device_id); + public static native String nativeGetHint(String name); + + /** + * This method is called by SDL using JNI. + */ + public static boolean setActivityTitle(String title) { + // Called from SDLMain() thread and can't directly affect the view + return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean sendMessage(int command, int param) { + return mSingleton.sendCommand(command, Integer.valueOf(param)); + } + + /** + * This method is called by SDL using JNI. + */ + public static Context getContext() { + return mSingleton; + } + + /** + * This method is called by SDL using JNI. + * @return result of getSystemService(name) but executed on UI thread. + */ + public Object getSystemServiceFromUiThread(final String name) { + final Object lock = new Object(); + final Object[] results = new Object[2]; // array for writable variables + synchronized (lock) { + runOnUiThread(new Runnable() { + @Override + public void run() { + synchronized (lock) { + results[0] = getSystemService(name); + results[1] = Boolean.TRUE; + lock.notify(); + } + } + }); + if (results[1] == null) { + try { + lock.wait(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + } + return results[0]; + } + + static class ShowTextInputTask implements Runnable { + /* + * This is used to regulate the pan&scan method to have some offset from + * the bottom edge of the input region and the top edge of an input + * method (soft keyboard) + */ + static final int HEIGHT_PADDING = 15; + + public int x, y, w, h; + + public ShowTextInputTask(int x, int y, int w, int h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + + @Override + public void run() { + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); + params.leftMargin = x; + params.topMargin = y; + + if (mTextEdit == null) { + mTextEdit = new DummyEdit(getContext()); + + mLayout.addView(mTextEdit, params); + } else { + mTextEdit.setLayoutParams(params); + } + + mTextEdit.setVisibility(View.VISIBLE); + mTextEdit.requestFocus(); + + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mTextEdit, 0); + } + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean showTextInput(int x, int y, int w, int h) { + // Transfer the task to the main thread as a Runnable + return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); + } + + /** + * This method is called by SDL using JNI. + */ + public static Surface getNativeSurface() { + return SDLActivity.mSurface.getNativeSurface(); + } + + // Audio + + /** + * This method is called by SDL using JNI. + */ + public static int audioOpen(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) { + int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO; + int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT; + int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1); + + Log.v(TAG, "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer"); + + // Let the user pick a larger buffer if they really want -- but ye + // gods they probably shouldn't, the minimums are horrifyingly high + // latency already + desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize); + + if (mAudioTrack == null) { + mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, + channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); + + // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid + // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java + // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() + + if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { + Log.e(TAG, "Failed during initialization of Audio Track"); + mAudioTrack = null; + return -1; + } + + mAudioTrack.play(); + } + + 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"); + + return 0; + } + + /** + * This method is called by SDL using JNI. + */ + public static void audioWriteShortBuffer(short[] buffer) { + for (int i = 0; i < buffer.length; ) { + int result = mAudioTrack.write(buffer, i, buffer.length - i); + if (result > 0) { + i += result; + } else if (result == 0) { + try { + Thread.sleep(1); + } catch(InterruptedException e) { + // Nom nom + } + } else { + Log.w(TAG, "SDL audio: error return from write(short)"); + return; + } + } + } + + /** + * This method is called by SDL using JNI. + */ + public static void audioWriteByteBuffer(byte[] buffer) { + for (int i = 0; i < buffer.length; ) { + int result = mAudioTrack.write(buffer, i, buffer.length - i); + if (result > 0) { + i += result; + } else if (result == 0) { + try { + Thread.sleep(1); + } catch(InterruptedException e) { + // Nom nom + } + } else { + Log.w(TAG, "SDL audio: error return from write(byte)"); + return; + } + } + } + + /** + * This method is called by SDL using JNI. + */ + public static int captureOpen(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) { + int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO; + int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT; + int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1); + + Log.v(TAG, "SDL capture: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer"); + + // Let the user pick a larger buffer if they really want -- but ye + // gods they probably shouldn't, the minimums are horrifyingly high + // latency already + desiredFrames = Math.max(desiredFrames, (AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize); + + if (mAudioRecord == null) { + mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate, + channelConfig, audioFormat, desiredFrames * frameSize); + + // see notes about AudioTrack state in audioOpen(), above. Probably also applies here. + if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) { + Log.e(TAG, "Failed during initialization of AudioRecord"); + mAudioRecord.release(); + mAudioRecord = null; + return -1; + } + + mAudioRecord.startRecording(); + } + + Log.v(TAG, "SDL capture: got " + ((mAudioRecord.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioRecord.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + (mAudioRecord.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer"); + + return 0; + } + + /** This method is called by SDL using JNI. */ + public static int captureReadShortBuffer(short[] buffer, boolean blocking) { + // !!! FIXME: this is available in API Level 23. Until then, we always block. :( + //return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); + return mAudioRecord.read(buffer, 0, buffer.length); + } + + /** This method is called by SDL using JNI. */ + public static int captureReadByteBuffer(byte[] buffer, boolean blocking) { + // !!! FIXME: this is available in API Level 23. Until then, we always block. :( + //return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); + return mAudioRecord.read(buffer, 0, buffer.length); + } + + + /** This method is called by SDL using JNI. */ + public static void audioClose() { + if (mAudioTrack != null) { + mAudioTrack.stop(); + mAudioTrack.release(); + mAudioTrack = null; + } + } + + /** This method is called by SDL using JNI. */ + public static void captureClose() { + if (mAudioRecord != null) { + mAudioRecord.stop(); + mAudioRecord.release(); + mAudioRecord = null; + } + } + + + // Input + + /** + * This method is called by SDL using JNI. + * @return an array which may be empty but is never null. + */ + public static int[] inputGetInputDeviceIds(int sources) { + int[] ids = InputDevice.getDeviceIds(); + int[] filtered = new int[ids.length]; + int used = 0; + for (int i = 0; i < ids.length; ++i) { + InputDevice device = InputDevice.getDevice(ids[i]); + if ((device != null) && ((device.getSources() & sources) != 0)) { + filtered[used++] = device.getId(); + } + } + return Arrays.copyOf(filtered, used); + } + + // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance + public static boolean handleJoystickMotionEvent(MotionEvent event) { + return mJoystickHandler.handleMotionEvent(event); + } + + /** + * This method is called by SDL using JNI. + */ + public static void pollInputDevices() { + if (SDLActivity.mSDLThread != null) { + mJoystickHandler.pollInputDevices(); + } + } + + // Check if a given device is considered a possible SDL joystick + public static boolean isDeviceSDLJoystick(int deviceId) { + InputDevice device = InputDevice.getDevice(deviceId); + // We cannot use InputDevice.isVirtual before API 16, so let's accept + // only nonnegative device ids (VIRTUAL_KEYBOARD equals -1) + if ((device == null) || (deviceId < 0)) { + return false; + } + int sources = device.getSources(); + return (((sources & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) || + ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) || + ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) + ); + } + + // APK expansion files support + + /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */ + private Object expansionFile; + + /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */ + private Method expansionFileMethod; + + /** + * This method is called by SDL using JNI. + * @return an InputStream on success or null if no expansion file was used. + * @throws IOException on errors. Message is set for the SDL error message. + */ + public InputStream openAPKExpansionInputStream(String fileName) throws IOException { + // Get a ZipResourceFile representing a merger of both the main and patch files + if (expansionFile == null) { + String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION"); + if (mainHint == null) { + return null; // no expansion use if no main version was set + } + String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION"); + if (patchHint == null) { + return null; // no expansion use if no patch version was set + } + + Integer mainVersion; + Integer patchVersion; + try { + mainVersion = Integer.valueOf(mainHint); + patchVersion = Integer.valueOf(patchHint); + } catch (NumberFormatException ex) { + ex.printStackTrace(); + throw new IOException("No valid file versions set for APK expansion files", ex); + } + + try { + // To avoid direct dependency on Google APK expansion library that is + // not a part of Android SDK we access it using reflection + expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport") + .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class) + .invoke(null, this, mainVersion, patchVersion); + + expansionFileMethod = expansionFile.getClass() + .getMethod("getInputStream", String.class); + } catch (Exception ex) { + ex.printStackTrace(); + expansionFile = null; + expansionFileMethod = null; + throw new IOException("Could not access APK expansion support library", ex); + } + } + + // Get an input stream for a known file inside the expansion file ZIPs + InputStream fileStream; + try { + fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName); + } catch (Exception ex) { + // calling "getInputStream" failed + ex.printStackTrace(); + throw new IOException("Could not open stream from APK expansion file", ex); + } + + if (fileStream == null) { + // calling "getInputStream" was successful but null was returned + throw new IOException("Could not find path in APK expansion file"); + } + + return fileStream; + } + + // Messagebox + + /** Result of current messagebox. Also used for blocking the calling thread. */ + protected final int[] messageboxSelection = new int[1]; + + /** Id of current dialog. */ + protected int dialogs = 0; + + /** + * This method is called by SDL using JNI. + * Shows the messagebox from UI thread and block calling thread. + * buttonFlags, buttonIds and buttonTexts must have same length. + * @param buttonFlags array containing flags for every button. + * @param buttonIds array containing id for every button. + * @param buttonTexts array containing text for every button. + * @param colors null for default or array of length 5 containing colors. + * @return button id or -1. + */ + public int messageboxShowMessageBox( + final int flags, + final String title, + final String message, + final int[] buttonFlags, + final int[] buttonIds, + final String[] buttonTexts, + final int[] colors) { + + messageboxSelection[0] = -1; + + // sanity checks + + if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { + return -1; // implementation broken + } + + // collect arguments for Dialog + + final Bundle args = new Bundle(); + args.putInt("flags", flags); + args.putString("title", title); + args.putString("message", message); + args.putIntArray("buttonFlags", buttonFlags); + args.putIntArray("buttonIds", buttonIds); + args.putStringArray("buttonTexts", buttonTexts); + args.putIntArray("colors", colors); + + // trigger Dialog creation on UI thread + + runOnUiThread(new Runnable() { + @Override + public void run() { + showDialog(dialogs++, args); + } + }); + + // block the calling thread + + synchronized (messageboxSelection) { + try { + messageboxSelection.wait(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + return -1; + } + } + + // return selected value + + return messageboxSelection[0]; + } + + @Override + protected Dialog onCreateDialog(int ignore, Bundle args) { + + // TODO set values from "flags" to messagebox dialog + + // get colors + + int[] colors = args.getIntArray("colors"); + int backgroundColor; + int textColor; + int buttonBorderColor; + int buttonBackgroundColor; + int buttonSelectedColor; + if (colors != null) { + int i = -1; + backgroundColor = colors[++i]; + textColor = colors[++i]; + buttonBorderColor = colors[++i]; + buttonBackgroundColor = colors[++i]; + buttonSelectedColor = colors[++i]; + } else { + backgroundColor = Color.TRANSPARENT; + textColor = Color.TRANSPARENT; + buttonBorderColor = Color.TRANSPARENT; + buttonBackgroundColor = Color.TRANSPARENT; + buttonSelectedColor = Color.TRANSPARENT; + } + + // create dialog with title and a listener to wake up calling thread + + final Dialog dialog = new Dialog(this); + dialog.setTitle(args.getString("title")); + dialog.setCancelable(false); + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface unused) { + synchronized (messageboxSelection) { + messageboxSelection.notify(); + } + } + }); + + // create text + + TextView message = new TextView(this); + message.setGravity(Gravity.CENTER); + message.setText(args.getString("message")); + if (textColor != Color.TRANSPARENT) { + message.setTextColor(textColor); + } + + // create buttons + + int[] buttonFlags = args.getIntArray("buttonFlags"); + int[] buttonIds = args.getIntArray("buttonIds"); + String[] buttonTexts = args.getStringArray("buttonTexts"); + + final SparseArray