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 0000000000..47ffead926
Binary files /dev/null and b/src/openrct2-android/app/external/debug.keystore differ
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