From 8766ec37a143d41398cb8cf2e316b752935a0060 Mon Sep 17 00:00:00 2001 From: Marijn van der Werf Date: Thu, 15 Jun 2017 14:22:15 +0200 Subject: [PATCH] Add Android project --- .gitignore | 3 +- .travis.yml | 36 + src/openrct2-android/.gitignore | 39 + src/openrct2-android/app/build.gradle | 102 + .../app/external/debug.keystore | Bin 0 -> 1259 bytes src/openrct2-android/app/google-services.json | 96 + .../app/src/main/AndroidManifest.xml | 33 + .../app/src/main/CMakeLists.txt | 123 ++ .../main/java/org/libsdl/app/SDLActivity.java | 1742 +++++++++++++++++ .../java/website/openrct2/GameActivity.java | 31 + .../java/website/openrct2/Localisation.java | 18 + .../java/website/openrct2/MainActivity.java | 195 ++ .../java/website/openrct2/OpenRCT2App.java | 24 + .../java/website/openrct2/ZipArchive.java | 107 + .../src/main/res/drawable-mdpi/logo_icon.png | Bin 0 -> 4582 bytes .../src/main/res/drawable-mdpi/logo_text.png | Bin 0 -> 5334 bytes .../app/src/main/res/layout/activity_main.xml | 34 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 5553 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2983 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 8094 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 13784 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 20080 bytes .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 19 + src/openrct2-android/build.gradle | 23 + src/openrct2-android/gradle.properties | 18 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + src/openrct2-android/gradlew | 160 ++ src/openrct2-android/gradlew.bat | 90 + src/openrct2-android/settings.gradle | 1 + src/openrct2-ui/Ui.cpp | 10 + src/openrct2-ui/UiContext.Android.cpp | 79 + src/openrct2-ui/UiContext.Linux.cpp | 2 +- src/openrct2/Version.h | 15 +- src/openrct2/config/Config.cpp | 2 +- src/openrct2/core/FileStream.hpp | 4 +- src/openrct2/core/Zip.cpp | 3 + src/openrct2/core/ZipAndroid.cpp | 179 ++ src/openrct2/diagnostic.c | 40 + src/openrct2/localisation/localisation.c | 23 + src/openrct2/platform/android.c | 90 + src/openrct2/platform/linux.c | 2 +- src/openrct2/platform/platform.h | 4 +- src/openrct2/platform/shared.c | 6 + src/openrct2/title/TitleSequence.cpp | 2 + 47 files changed, 3361 insertions(+), 9 deletions(-) create mode 100644 src/openrct2-android/.gitignore create mode 100644 src/openrct2-android/app/build.gradle create mode 100644 src/openrct2-android/app/external/debug.keystore create mode 100644 src/openrct2-android/app/google-services.json create mode 100644 src/openrct2-android/app/src/main/AndroidManifest.xml create mode 100644 src/openrct2-android/app/src/main/CMakeLists.txt create mode 100644 src/openrct2-android/app/src/main/java/org/libsdl/app/SDLActivity.java create mode 100644 src/openrct2-android/app/src/main/java/website/openrct2/GameActivity.java create mode 100644 src/openrct2-android/app/src/main/java/website/openrct2/Localisation.java create mode 100644 src/openrct2-android/app/src/main/java/website/openrct2/MainActivity.java create mode 100644 src/openrct2-android/app/src/main/java/website/openrct2/OpenRCT2App.java create mode 100644 src/openrct2-android/app/src/main/java/website/openrct2/ZipArchive.java create mode 100644 src/openrct2-android/app/src/main/res/drawable-mdpi/logo_icon.png create mode 100644 src/openrct2-android/app/src/main/res/drawable-mdpi/logo_text.png create mode 100644 src/openrct2-android/app/src/main/res/layout/activity_main.xml create mode 100644 src/openrct2-android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/openrct2-android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/openrct2-android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/openrct2-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/openrct2-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/openrct2-android/app/src/main/res/values/colors.xml create mode 100644 src/openrct2-android/app/src/main/res/values/strings.xml create mode 100644 src/openrct2-android/app/src/main/res/values/styles.xml create mode 100644 src/openrct2-android/build.gradle create mode 100644 src/openrct2-android/gradle.properties create mode 100644 src/openrct2-android/gradle/wrapper/gradle-wrapper.jar create mode 100644 src/openrct2-android/gradle/wrapper/gradle-wrapper.properties create mode 100755 src/openrct2-android/gradlew create mode 100644 src/openrct2-android/gradlew.bat create mode 100644 src/openrct2-android/settings.gradle create mode 100644 src/openrct2-ui/UiContext.Android.cpp create mode 100644 src/openrct2/core/ZipAndroid.cpp create mode 100644 src/openrct2/platform/android.c diff --git a/.gitignore b/.gitignore index 0871d9c032..e95f22ec18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,13 @@ # OPENRCT2 -sdl +/sdl # Compiled dll openrct2.dll # Distribution distribution/windows/*.exe +distribution/android/*/external/ # Build artifacts artifacts diff --git a/.travis.yml b/.travis.yml index 97fbac1a27..9d969e24e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -86,6 +86,42 @@ matrix: curl -o - -v --form "key=$OPENRCT2_ORG_TOKEN" --form "fileName=OpenRCT2-${OPENRCT2_VERSION}${FILENAME_PART}-macos.zip" --form "version=${OPENRCT2_VERSION}" --form "gitHash=$TRAVIS_COMMIT" --form "gitBranch=$PUSH_BRANCH" --form "flavourId=3" --form "file=@openrct2-macos.zip" "https://openrct2.org/altapi/?command=push-build"; else curl --progress-bar --upload-file openrct2-macos.zip https://transfer.sh/openrct2-macos.zip -o link && cat link; fi + - os: linux + language: android + dist: precise + before_install: [] + addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - libstdc++6-4.7-dev + android: + components: + - build-tools-25.0.2 + jdk: oraclejdk8 + before_script: + - pushd ~ + - wget https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip + - unzip -qo sdk-tools-linux-3859397.zip + - rm -Rf "$ANDROID_HOME/tools" + - mv tools "$ANDROID_HOME/tools" + - popd + - 'echo "count=0" > ~/.android/repositories.cfg' + - '"$ANDROID_HOME/tools/bin/sdkmanager" --list' + - 'echo y | "$ANDROID_HOME/tools/bin/sdkmanager" platform-tools' + - 'echo y | "$ANDROID_HOME/tools/bin/sdkmanager" "platforms;android-25"' + - 'echo y | "$ANDROID_HOME/tools/bin/sdkmanager" "cmake;3.6.3155560"' + - '"$ANDROID_HOME/tools/bin/sdkmanager" ndk-bundle' + - '"$ANDROID_HOME/tools/bin/sdkmanager" --list' + - 'export ANDROID_NDK_HOME="$ANDROID_HOME/ndk-bundle"' + - 'cd src/openrct2-android' + - TERM=dumb # Makes Gradle use 'boring' output + script: + - './gradlew app:assemblePR' + after_success: + - curl --progress-bar --upload-file app/build/outputs/apk/app-arm-pr.apk https://transfer.sh/openrct2-android-arm.apk -o link && cat link + - curl --progress-bar --upload-file app/build/outputs/apk/app-x86-pr.apk https://transfer.sh/openrct2-android-x86.apk -o link && cat link # Following entries used to be included in testing, but they only proved useful while changing things in CMake setup. # They are meant to be used when there are changes to CMakeLists.txt # - os: linux diff --git a/src/openrct2-android/.gitignore b/src/openrct2-android/.gitignore new file mode 100644 index 0000000000..a836875126 --- /dev/null +++ b/src/openrct2-android/.gitignore @@ -0,0 +1,39 @@ +# Built application files +*.apk +*.ap_ + +# Files for the Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Intellij +*.iml + +# Keystore files +*.jks diff --git a/src/openrct2-android/app/build.gradle b/src/openrct2-android/app/build.gradle new file mode 100644 index 0000000000..0f3c092a33 --- /dev/null +++ b/src/openrct2-android/app/build.gradle @@ -0,0 +1,102 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion '25.0.2' + + defaultConfig { + applicationId 'website.openrct2' + minSdkVersion 16 + targetSdkVersion 25 + + versionCode 2 + versionName '0.0.8' + + externalNativeBuild { + cmake { + arguments '-DANDROID_STL=c++_shared' + targets 'openrct2', 'openrct2-ui' + } + } + } + + signingConfigs { + debug { + storeFile file('external/debug.keystore') + } + } + + buildTypes { + debug { + applicationIdSuffix '.debug' + versionNameSuffix '-DEBUG' + } + pr { + applicationIdSuffix '.debug' + signingConfig signingConfigs.debug + } + develop { + applicationIdSuffix '.develop' + versionNameSuffix '-DEVELOP' + signingConfig signingConfigs.debug + } + release { + signingConfig signingConfigs.debug + } + } + + externalNativeBuild { + cmake { + path 'src/main/CMakeLists.txt' + } + } + + productFlavors { + arm7 { + ndk { + abiFilters 'armeabi-v7a' + } + } + arm { + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a' + } + } + x86 { + ndk { + abiFilters 'x86', 'x86_64' + } + } + } +} + +apply plugin: 'de.undercouch.download' + +android.applicationVariants.all { variant -> + variant.mergeAssets.doLast { + copy { + from '../../../data' + into "$variant.mergeAssets.outputDir/data" + } + download { + src 'https://github.com/marijnvdwerf/openrct2-dependencies-android/releases/download/v0.7/g2.dat' + dest "$variant.mergeAssets.outputDir/data" + } + download { + src 'https://github.com/OpenRCT2/title-sequences/releases/download/v0.0.5/title-sequence-v0.0.5.zip' + dest new File(buildDir, 'title-sequence.zip') + } + copy { + from zipTree(new File(buildDir, 'title-sequence.zip')) + into "$variant.mergeAssets.outputDir/data/title" + } + } +} + +dependencies { + compile 'commons-io:commons-io:2.5' + compile 'com.android.support:appcompat-v7:25.3.1' + compile 'com.google.android.gms:play-services-analytics:10.2.1' +} + +apply plugin: 'com.google.gms.google-services' diff --git a/src/openrct2-android/app/external/debug.keystore b/src/openrct2-android/app/external/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..47ffead926a4e252bb0e347f2d25a4ab3ba635c9 GIT binary patch literal 1259 zcmezO_TO6u1_mY|W&~sY#JrTE{LGY;)TGk%?9@u2cyy6`u>nxcE`uhfZ3cX7T-t1m zER0%Af{cu;3@lAdi_)hCefoK@>eM;mHp^F&Je|}(tEvZYd9Wn$a9614n*MzE3I3fY zwoYt~?v1`R-*gt!(^(1m^K>zVcbZO6?;QT=w-7GHLH3jegTFgQ-`VIxZ?}>@@Z)=w5GFZDIP~mZmlF7GgPtHA9W~6yZr`Xp$WZm5aExW0rUzg23 zenM!{;&sxypPw%lvIIM$*OU_l)au+I3qC zn9|j!-}OF}!l|?2yLO(EXS{>XfOY{X4-~}5hgF$ zP6kMQH-0vELLtB3qe*2d-qZF7W>>8^f57)n`@1VTF(=+X?YeGb{OgD9y2kp)3zycv zzY#1hwf@9FC*R0hDuGGKG$N6&7XTpZWB-YU2$mjTwbEHdFJP*zWb9dPYa5C zma%c}$?Rm>sJ!cM;T>3Fd9 zxcgSIW78&^FOBr z^F4p3P(<{Ja}~QD<~%#kkYA+mtdhBVM%Ll8Ug1aVs{^EFR?OC9cyv@|=gtF9cAXV9 zxV%KSeA?53(+&)|^PH`V(^F=DSh~1r!h>XZW{S^;rG%T;H1qO zp=WAf2~5xr4VoD50d#AN85K^U}|P%Ffcb1GZ0~84rO8H;dO*$1qByS zUN95@CVikd7Y{pB+>qOV6C}bW%oG}IAScdiXl`I)XliI;YGG^|CC+OC?G|F9e9Q_^;VDbF&p@*9mbJwd?u9vdC+AB5R*gkhr%Y{XHoB4PHV)i}AWnyMz zKo3G@pu2?nPMS J-_c&lNdORy5}N=3 literal 0 HcmV?d00001 diff --git a/src/openrct2-android/app/google-services.json b/src/openrct2-android/app/google-services.json new file mode 100644 index 0000000000..6c7906a29a --- /dev/null +++ b/src/openrct2-android/app/google-services.json @@ -0,0 +1,96 @@ +{ + "project_info": { + "project_number": "", + "project_id": "" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "", + "android_client_info": { + "package_name": "website.openrct2.debug" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "" + } + ], + "services": { + "analytics_service": { + "status": 2, + "analytics_property": { + "tracking_id": "UA-XXXXXXXX-1" + } + }, + "appinvite_service": { + "status": 1, + "other_platform_oauth_client": [] + }, + "ads_service": { + "status": 1 + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "", + "android_client_info": { + "package_name": "website.openrct2.develop" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "" + } + ], + "services": { + "analytics_service": { + "status": 2, + "analytics_property": { + "tracking_id": "UA-XXXXXXXX-1" + } + }, + "appinvite_service": { + "status": 1, + "other_platform_oauth_client": [] + }, + "ads_service": { + "status": 1 + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "", + "android_client_info": { + "package_name": "website.openrct2" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "" + } + ], + "services": { + "analytics_service": { + "status": 2, + "analytics_property": { + "tracking_id": "UA-XXXXXXXX-1" + } + }, + "appinvite_service": { + "status": 1, + "other_platform_oauth_client": [] + }, + "ads_service": { + "status": 1 + } + } + } +], + "configuration_version": "1" +} diff --git a/src/openrct2-android/app/src/main/AndroidManifest.xml b/src/openrct2-android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..923f4ef6b3 --- /dev/null +++ b/src/openrct2-android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/openrct2-android/app/src/main/CMakeLists.txt b/src/openrct2-android/app/src/main/CMakeLists.txt new file mode 100644 index 0000000000..286d8c1906 --- /dev/null +++ b/src/openrct2-android/app/src/main/CMakeLists.txt @@ -0,0 +1,123 @@ +cmake_minimum_required(VERSION 3.6.0) + +set(CMAKE_VERBOSE_MAKEFILE on) + +set(lib_src_DIR ${CMAKE_SOURCE_DIR}/../../../libs) +set(lib_build_DIR $ENV{HOME}/tmp) +file(MAKE_DIRECTORY ${lib_build_DIR}) + +set(DEBUG_LEVEL 0 CACHE STRING "Select debug level for compilation. Use value in range 0–3.") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DDEBUG=${DEBUG_LEVEL}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDEBUG=${DEBUG_LEVEL}") + +include(ExternalProject) +ExternalProject_Add(libs + URL https://github.com/marijnvdwerf/openrct2-dependencies-android/releases/download/v0.7/openrct2-libs-android-${ANDROID_ABI}.zip + + SOURCE_DIR "${CMAKE_BINARY_DIR}/libs" + + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + + BUILD_BYPRODUCTS + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}freetype${CMAKE_SHARED_LIBRARY_SUFFIX} + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}jansson${CMAKE_SHARED_LIBRARY_SUFFIX} + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}png16${CMAKE_SHARED_LIBRARY_SUFFIX} + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}SDL2-2.0${CMAKE_SHARED_LIBRARY_SUFFIX} + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_STATIC_LIBRARY_PREFIX}SDL2main${CMAKE_STATIC_LIBRARY_SUFFIX} + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}speexdsp${CMAKE_SHARED_LIBRARY_SUFFIX} + + LOG_DOWNLOAD 1 + LOG_UPDATE 1 + LOG_CONFIGURE 1 + LOG_BUILD 1 + LOG_TEST 1 + LOG_INSTALL 1 +) + +add_custom_command(TARGET libs POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/libs/lib/*.so" ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} +) + +add_library(freetype SHARED IMPORTED) +set_target_properties(freetype PROPERTIES IMPORTED_LOCATION + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}freetype${CMAKE_SHARED_LIBRARY_SUFFIX} +) +add_dependencies(freetype libs) + +add_library(jansson SHARED IMPORTED) +set_target_properties(jansson PROPERTIES IMPORTED_LOCATION + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}jansson${CMAKE_SHARED_LIBRARY_SUFFIX} +) +add_dependencies(jansson libs) + +add_library(png SHARED IMPORTED) +set_target_properties(png PROPERTIES IMPORTED_LOCATION + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}png16${CMAKE_SHARED_LIBRARY_SUFFIX} +) +add_dependencies(png libs) + +add_library(SDL2 SHARED IMPORTED) +set_target_properties(SDL2 PROPERTIES IMPORTED_LOCATION + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}SDL2-2.0${CMAKE_SHARED_LIBRARY_SUFFIX} +) +add_dependencies(SDL2 libs) + +add_library(SDL2main STATIC IMPORTED) +set_target_properties(SDL2main PROPERTIES IMPORTED_LOCATION + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_STATIC_LIBRARY_PREFIX}SDL2main${CMAKE_STATIC_LIBRARY_SUFFIX} +) +add_dependencies(SDL2main libs) + +add_library(speexdsp SHARED IMPORTED) +set_target_properties(speexdsp PROPERTIES IMPORTED_LOCATION + ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}speexdsp${CMAKE_SHARED_LIBRARY_SUFFIX} +) +add_dependencies(speexdsp libs) + + +include_directories("${CMAKE_BINARY_DIR}/libs/include") +include_directories("${CMAKE_BINARY_DIR}/libs/include/freetype2") +include_directories("${CMAKE_BINARY_DIR}/libs/include/SDL2") + +# now build app's shared lib +include_directories(./ndk_helper + ${ANDROID_NDK}/sources/android/cpufeatures) +add_definitions(-DDISABLE_HTTP -DDISABLE_TWITCH -DDISABLE_NETWORK -DDISABLE_OPENGL -DGL_GLEXT_PROTOTYPES -D__STDC_LIMIT_MACROS -DNO_RCT2 -DNO_TTF -DSDL_MAIN_HANDLED) + +# Fix SpeexDSP compilation +add_definitions(-DHAVE_STDINT_H) + +set(COMMON_COMPILE_OPTIONS "${COMMON_COMPILE_OPTIONS} -fstrict-aliasing -Werror -Wundef -Wmissing-declarations -Winit-self -Wall -Wno-unknown-pragmas -Wno-unused-function -Wno-missing-braces ") +set(COMMON_COMPILE_OPTIONS "${COMMON_COMPILE_OPTIONS} -Wno-comment -Wshadow -Wmissing-declarations -Wnonnull") +set(COMMON_COMPILE_OPTIONS "${COMMON_COMPILE_OPTIONS} -fPIC") + +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--undefined=Java_org_libsdl_app_SDLActivity_nativeInit") +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu11 ${COMMON_COMPILE_OPTIONS} -Wimplicit") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++14 ${COMMON_COMPILE_OPTIONS} -Wnon-virtual-dtor") +get_filename_component(ORCT2_ROOT "${CMAKE_SOURCE_DIR}/../../../../../" REALPATH) + +file(GLOB_RECURSE LIBOPENRCT2_SOURCES + "${ORCT2_ROOT}/src/openrct2/*.c" + "${ORCT2_ROOT}/src/openrct2/*.cpp" + "${ORCT2_ROOT}/src/openrct2/*.h" + "${ORCT2_ROOT}/src/openrct2/*.hpp") + +file(GLOB_RECURSE OPENRCT2_GUI_SOURCES + "${ORCT2_ROOT}/src/openrct2-ui/*.c" + "${ORCT2_ROOT}/src/openrct2-ui/*.cpp" + "${ORCT2_ROOT}/src/openrct2-ui/*.h" + "${ORCT2_ROOT}/src/openrct2-ui/*.hpp") + +add_library(openrct2 SHARED ${LIBOPENRCT2_SOURCES}) +target_link_libraries(openrct2 + android log dl GLESv1_CM GLESv2 z + SDL2 png jansson speexdsp + ) + +add_library(openrct2-ui SHARED ${OPENRCT2_GUI_SOURCES}) +target_link_libraries(openrct2-ui openrct2 SDL2main) +target_include_directories(openrct2-ui PRIVATE "${ORCT2_ROOT}/src") \ No newline at end of file diff --git a/src/openrct2-android/app/src/main/java/org/libsdl/app/SDLActivity.java b/src/openrct2-android/app/src/main/java/org/libsdl/app/SDLActivity.java new file mode 100644 index 0000000000..c599ae6a49 --- /dev/null +++ b/src/openrct2-android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -0,0 +1,1742 @@ +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", + // "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