From: Holger Schemel Date: Fri, 3 Jan 2020 00:34:36 +0000 (+0100) Subject: added Android project files X-Git-Tag: 4.1.4.0~4 X-Git-Url: https://git.artsoft.org/?a=commitdiff_plain;h=feeeabcbf6696f27ddd68ad56d19db1660a6c4fe;p=rocksndiamonds.git 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. --- 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 00000000..9ed04d0f Binary files /dev/null and b/build-projects/android/res/drawable-hdpi/ic_launcher.png differ 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 00000000..bf733ec5 Binary files /dev/null and b/build-projects/android/res/drawable-ldpi/ic_launcher.png differ 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 00000000..560306ac Binary files /dev/null and b/build-projects/android/res/drawable-mdpi/ic_launcher.png differ diff --git a/build-projects/android/res/drawable-xhdpi/ic_launcher.png b/build-projects/android/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..4c31ddeb Binary files /dev/null and b/build-projects/android/res/drawable-xhdpi/ic_launcher.png differ diff --git a/build-projects/android/res/drawable-xxhdpi/ic_launcher.png b/build-projects/android/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..132d3c13 Binary files /dev/null and b/build-projects/android/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/build-projects/android/res/layout/main.xml b/build-projects/android/res/layout/main.xml new file mode 100644 index 00000000..5838df11 --- /dev/null +++ b/build-projects/android/res/layout/main.xml @@ -0,0 +1,12 @@ + + + + + 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