diff --git a/include/eepp/ui/actions/delay.hpp b/include/eepp/ui/actions/delay.hpp index e128b3f0f..4b3b8d12f 100644 --- a/include/eepp/ui/actions/delay.hpp +++ b/include/eepp/ui/actions/delay.hpp @@ -19,9 +19,9 @@ class EE_API Delay : public UIAction { bool isDone() override; - UIAction * clone() const; + UIAction * clone() const override; - UIAction * reverse() const; + UIAction * reverse() const override; protected: Clock mClock; diff --git a/include/eepp/ui/actions/fade.hpp b/include/eepp/ui/actions/fade.hpp index 49760a132..a634ccdf2 100644 --- a/include/eepp/ui/actions/fade.hpp +++ b/include/eepp/ui/actions/fade.hpp @@ -10,15 +10,15 @@ class EE_API Fade : public ActionInterpolation1d { public: static Fade * New( const Float& start, const Float& end, const Time& duration, const Ease::Interpolation& type = Ease::Linear, const bool& alphaChilds = true ); - UIAction * clone() const; + UIAction * clone() const override; - UIAction * reverse() const; + UIAction * reverse() const override; protected: Fade( const Float & start, const Float & end, const Time & duration, const Ease::Interpolation & type, const bool& alphaChilds ); - void onStart(); + void onStart() override; - void onUpdate( const Time& time ); + void onUpdate( const Time& time ) override; bool mAffectChilds; private: diff --git a/include/eepp/ui/actions/marginmove.hpp b/include/eepp/ui/actions/marginmove.hpp index 62c17fa19..1b5648f43 100644 --- a/include/eepp/ui/actions/marginmove.hpp +++ b/include/eepp/ui/actions/marginmove.hpp @@ -21,9 +21,9 @@ class EE_API MarginMove : public UIAction { bool isDone() override; - UIAction * clone() const; + UIAction * clone() const override; - UIAction * reverse() const; + UIAction * reverse() const override; Interpolation1d getInterpolationLeft() const; @@ -44,9 +44,9 @@ class EE_API MarginMove : public UIAction { protected: MarginMove( const Rect & start, const Rect & end, const Time & duration, const Ease::Interpolation & type ); - void onStart(); + void onStart() override; - void onUpdate( const Time& time ); + void onUpdate( const Time& time ) override; private: MarginMove(); diff --git a/include/eepp/ui/actions/move.hpp b/include/eepp/ui/actions/move.hpp index b13fb6526..cc580f13c 100644 --- a/include/eepp/ui/actions/move.hpp +++ b/include/eepp/ui/actions/move.hpp @@ -10,15 +10,15 @@ class EE_API Move : public ActionInterpolation2d { public: static Move * New( const Vector2f& start, const Vector2f& end, const Time& duration, const Ease::Interpolation& type = Ease::Linear ); - UIAction * clone() const; + UIAction * clone() const override; - UIAction * reverse() const; + UIAction * reverse() const override; protected: Move( const Vector2f& start, const Vector2f& end, const Time& duration, const Ease::Interpolation& type ); - void onStart(); + void onStart() override; - void onUpdate( const Time& time ); + void onUpdate( const Time& time ) override; private: Move(); }; diff --git a/include/eepp/ui/actions/rotate.hpp b/include/eepp/ui/actions/rotate.hpp index 691ecd77d..617d9be6d 100644 --- a/include/eepp/ui/actions/rotate.hpp +++ b/include/eepp/ui/actions/rotate.hpp @@ -10,15 +10,15 @@ class EE_API Rotate : public ActionInterpolation1d { public: static Rotate * New( const Float& start, const Float& end, const Time& duration, const Ease::Interpolation& type = Ease::Linear ); - UIAction * clone() const; + UIAction * clone() const override; - UIAction * reverse() const; + UIAction * reverse() const override; protected: Rotate( const Float & start, const Float & end, const Time & duration, const Ease::Interpolation & type ); - void onStart(); + void onStart() override; - void onUpdate( const Time& time ); + void onUpdate( const Time& time ) override; private: Rotate(); }; diff --git a/include/eepp/ui/actions/scale.hpp b/include/eepp/ui/actions/scale.hpp index 1606102ad..fe917863d 100644 --- a/include/eepp/ui/actions/scale.hpp +++ b/include/eepp/ui/actions/scale.hpp @@ -10,15 +10,15 @@ class EE_API Scale : public ActionInterpolation2d { public: static Scale * New( const Vector2f& start, const Vector2f& end, const Time& duration, const Ease::Interpolation& type = Ease::Linear ); - UIAction * clone() const; + UIAction * clone() const override; - UIAction * reverse() const; + UIAction * reverse() const override; protected: Scale( const Vector2f& start, const Vector2f& end, const Time& duration, const Ease::Interpolation& type ); - void onStart(); + void onStart() override; - void onUpdate( const Time& time ); + void onUpdate( const Time& time ) override; private: Scale(); }; diff --git a/include/eepp/ui/actions/sequence.hpp b/include/eepp/ui/actions/sequence.hpp index 7cdbb60ed..c7980c703 100644 --- a/include/eepp/ui/actions/sequence.hpp +++ b/include/eepp/ui/actions/sequence.hpp @@ -25,9 +25,9 @@ class EE_API Sequence : public UIAction { bool isDone() override; - UIAction * clone() const; + UIAction * clone() const override; - UIAction * reverse() const; + UIAction * reverse() const override; virtual ~Sequence(); diff --git a/include/eepp/ui/actions/spawn.hpp b/include/eepp/ui/actions/spawn.hpp index 10ed69668..ae1f733be 100644 --- a/include/eepp/ui/actions/spawn.hpp +++ b/include/eepp/ui/actions/spawn.hpp @@ -25,9 +25,9 @@ class EE_API Spawn : public UIAction { bool isDone() override; - UIAction * clone() const; + UIAction * clone() const override; - UIAction * reverse() const; + UIAction * reverse() const override; virtual ~Spawn(); protected: diff --git a/projects/android-project-ant/jni/eepp.mk b/projects/android-project-ant/jni/eepp.mk index d7b36b9a4..5f9a20dcb 100644 --- a/projects/android-project-ant/jni/eepp.mk +++ b/projects/android-project-ant/jni/eepp.mk @@ -168,13 +168,13 @@ LOCAL_C_INCLUDES := $(LOCAL_PATH)/include LOCAL_CFLAGS := -D__ANDROID__ -DANDROID -DGL_GLEXT_PROTOTYPES $(EE_GLES_VERSION) -LOCAL_SRC_FILES := \ +LOCAL_SRC_FILES := \ $(subst $(LOCAL_PATH)/,, \ $(wildcard $(LOCAL_PATH)/src/*.c) \ $(wildcard $(LOCAL_PATH)/src/audio/*.c) \ $(wildcard $(LOCAL_PATH)/src/audio/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/audio/dummy/*.c) \ - $(LOCAL_PATH)/src/atomic/SDL_atomic.c \ + $(LOCAL_PATH)/src/atomic/SDL_atomic.c.arm \ $(LOCAL_PATH)/src/atomic/SDL_spinlock.c.arm \ $(wildcard $(LOCAL_PATH)/src/core/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/cpuinfo/*.c) \ @@ -182,13 +182,14 @@ LOCAL_SRC_FILES := \ $(wildcard $(LOCAL_PATH)/src/events/*.c) \ $(wildcard $(LOCAL_PATH)/src/file/*.c) \ $(wildcard $(LOCAL_PATH)/src/haptic/*.c) \ - $(wildcard $(LOCAL_PATH)/src/haptic/dummy/*.c) \ + $(wildcard $(LOCAL_PATH)/src/haptic/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/joystick/*.c) \ $(wildcard $(LOCAL_PATH)/src/joystick/android/*.c) \ + $(LOCAL_PATH)/src/joystick/steam/SDL_steamcontroller.c \ $(wildcard $(LOCAL_PATH)/src/loadso/dlopen/*.c) \ $(wildcard $(LOCAL_PATH)/src/power/*.c) \ $(wildcard $(LOCAL_PATH)/src/power/android/*.c) \ - $(wildcard $(LOCAL_PATH)/src/filesystem/dummy/*.c) \ + $(wildcard $(LOCAL_PATH)/src/filesystem/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/render/*.c) \ $(wildcard $(LOCAL_PATH)/src/render/*/*.c) \ $(wildcard $(LOCAL_PATH)/src/stdlib/*.c) \ @@ -198,7 +199,8 @@ LOCAL_SRC_FILES := \ $(wildcard $(LOCAL_PATH)/src/timer/unix/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/android/*.c) \ - $(wildcard $(LOCAL_PATH)/src/test/*.c)) + $(wildcard $(LOCAL_PATH)/src/video/yuv2rgb/*.c) \ + $(wildcard $(LOCAL_PATH)/src/test/*.c)) include $(BUILD_STATIC_LIBRARY) #**************** SDL 2 *************** diff --git a/projects/android-project/app/jni/eepp.mk b/projects/android-project/app/jni/eepp.mk index 34102efd5..96ae935ed 100644 --- a/projects/android-project/app/jni/eepp.mk +++ b/projects/android-project/app/jni/eepp.mk @@ -168,13 +168,13 @@ LOCAL_C_INCLUDES := $(LOCAL_PATH)/include LOCAL_CFLAGS := -D__ANDROID__ -DANDROID -DGL_GLEXT_PROTOTYPES $(EE_GLES_VERSION) -LOCAL_SRC_FILES := \ +LOCAL_SRC_FILES := \ $(subst $(LOCAL_PATH)/,, \ $(wildcard $(LOCAL_PATH)/src/*.c) \ $(wildcard $(LOCAL_PATH)/src/audio/*.c) \ $(wildcard $(LOCAL_PATH)/src/audio/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/audio/dummy/*.c) \ - $(LOCAL_PATH)/src/atomic/SDL_atomic.c \ + $(LOCAL_PATH)/src/atomic/SDL_atomic.c.arm \ $(LOCAL_PATH)/src/atomic/SDL_spinlock.c.arm \ $(wildcard $(LOCAL_PATH)/src/core/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/cpuinfo/*.c) \ @@ -182,13 +182,14 @@ LOCAL_SRC_FILES := \ $(wildcard $(LOCAL_PATH)/src/events/*.c) \ $(wildcard $(LOCAL_PATH)/src/file/*.c) \ $(wildcard $(LOCAL_PATH)/src/haptic/*.c) \ - $(wildcard $(LOCAL_PATH)/src/haptic/dummy/*.c) \ + $(wildcard $(LOCAL_PATH)/src/haptic/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/joystick/*.c) \ $(wildcard $(LOCAL_PATH)/src/joystick/android/*.c) \ + $(LOCAL_PATH)/src/joystick/steam/SDL_steamcontroller.c \ $(wildcard $(LOCAL_PATH)/src/loadso/dlopen/*.c) \ $(wildcard $(LOCAL_PATH)/src/power/*.c) \ $(wildcard $(LOCAL_PATH)/src/power/android/*.c) \ - $(wildcard $(LOCAL_PATH)/src/filesystem/dummy/*.c) \ + $(wildcard $(LOCAL_PATH)/src/filesystem/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/render/*.c) \ $(wildcard $(LOCAL_PATH)/src/render/*/*.c) \ $(wildcard $(LOCAL_PATH)/src/stdlib/*.c) \ @@ -198,7 +199,8 @@ LOCAL_SRC_FILES := \ $(wildcard $(LOCAL_PATH)/src/timer/unix/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/*.c) \ $(wildcard $(LOCAL_PATH)/src/video/android/*.c) \ - $(wildcard $(LOCAL_PATH)/src/test/*.c)) + $(wildcard $(LOCAL_PATH)/src/video/yuv2rgb/*.c) \ + $(wildcard $(LOCAL_PATH)/src/test/*.c)) include $(BUILD_STATIC_LIBRARY) #**************** SDL 2 *************** diff --git a/projects/android-project/app/src/main/AndroidManifest.xml b/projects/android-project/app/src/main/AndroidManifest.xml index 4c9aa7dd7..7b405c81e 100644 --- a/projects/android-project/app/src/main/AndroidManifest.xml +++ b/projects/android-project/app/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ + android:versionName="1.0" + android:installLocation="auto"> + + diff --git a/projects/android-project/app/src/main/java/org/libsdl/app/SDL.java b/projects/android-project/app/src/main/java/org/libsdl/app/SDL.java new file mode 100644 index 000000000..cfe483094 --- /dev/null +++ b/projects/android-project/app/src/main/java/org/libsdl/app/SDL.java @@ -0,0 +1,37 @@ +package org.libsdl.app; + +import android.content.Context; + +/** + SDL library initialization +*/ +public class SDL { + + // This function should be called first and sets up the native code + // so it can call into the Java classes + public static void setupJNI() { + SDLActivity.nativeSetupJNI(); + SDLAudioManager.nativeSetupJNI(); + SDLControllerManager.nativeSetupJNI(); + } + + // This function should be called each time the activity is started + public static void initialize() { + setContext(null); + + SDLActivity.initialize(); + SDLAudioManager.initialize(); + SDLControllerManager.initialize(); + } + + // This function stores the current activity (SDL or not) + public static void setContext(Context context) { + mContext = context; + } + + public static Context getContext() { + return mContext; + } + + protected static Context mContext; +} diff --git a/projects/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/projects/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java index d81b397a4..6c0a29436 100644 --- a/projects/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/projects/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -1,25 +1,32 @@ package org.libsdl.app; -import java.util.ArrayList; +import java.io.IOException; +import java.io.InputStream; 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.AbsoluteLayout; +import android.widget.RelativeLayout; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; import android.os.*; +import android.util.DisplayMetrics; import android.util.Log; +import android.util.SparseArray; import android.graphics.*; -import android.media.*; +import android.graphics.drawable.Drawable; import android.hardware.*; -import android.os.PowerManager; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ApplicationInfo; /** SDL Activity @@ -27,31 +34,92 @@ import android.os.PowerManager; 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 mIsResumedCalled, mIsSurfaceReady, mHasFocus; + + // Handle the state of the native layer + public enum NativeState { + INIT, RESUMED, PAUSED + } + + public static NativeState mNextNativeState; + public static NativeState mCurrentNativeState; + 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 boolean mScreenKeyboardShown; protected static ViewGroup mLayout; - protected static SDLJoystickHandler mJoystickHandler; + protected static SDLClipboardHandler mClipboardHandler; + // This is what SDL runs in. It invokes SDL_main(), eventually protected static Thread mSDLThread; - - // Audio - protected static AudioTrack mAudioTrack; - protected static PowerManager.WakeLock mWakeLock; + + /** + * This method returns the name of the shared object with the application entry point + * It can be overridden by derived classes. + */ + protected String getMainSharedObject() { + String library; + String[] libraries = SDLActivity.mSingleton.getLibraries(); + if (libraries.length > 0) { + library = "lib" + libraries[libraries.length - 1] + ".so"; + } else { + library = "libmain.so"; + } + return library; + } + + /** + * This method returns the name of the application entry point + * It can be overridden by derived classes. + */ + protected String getMainFunction() { + return "SDL_main"; + } + + /** + * 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[] { + "openal", + "main" + }; + } // Load the .so - static { - System.loadLibrary("openal"); - System.loadLibrary("main"); + 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 @@ -59,84 +127,187 @@ public class SDLActivity extends Activity { mSurface = null; mTextEdit = null; mLayout = null; - mJoystickHandler = null; + mClipboardHandler = null; mSDLThread = null; - mAudioTrack = null; mExitCalledFromJava = false; - mIsPaused = false; + mBrokenLibraries = false; + mIsResumedCalled = false; mIsSurfaceReady = false; mHasFocus = true; + mNextNativeState = NativeState.INIT; + mCurrentNativeState = NativeState.INIT; } // Setup @Override protected void onCreate(Bundle savedInstanceState) { - Log.v("SDL", "onCreate():" + mSingleton); + Log.v(TAG, "Device: " + android.os.Build.DEVICE); + Log.v(TAG, "Model: " + android.os.Build.MODEL); + Log.v(TAG, "onCreate()"); super.onCreate(savedInstanceState); - - SDLActivity.initialize(); + + // 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) + { + mSingleton = this; + 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 JNI + SDL.setupJNI(); + + // Initialize state + SDL.initialize(); + // So we can call stuff from static callbacks mSingleton = this; + SDL.setContext(this); + + if (Build.VERSION.SDK_INT >= 11) { + mClipboardHandler = new SDLClipboardHandler_API11(); + } else { + /* Before API 11, no clipboard notification (eg no SDL_CLIPBOARDUPDATE) */ + mClipboardHandler = new SDLClipboardHandler_Old(); + } // Set up the surface mSurface = new SDLSurface(getApplication()); - - if(Build.VERSION.SDK_INT >= 12) { - mJoystickHandler = new SDLJoystickHandler_API12(); - } - else { - mJoystickHandler = new SDLJoystickHandler(); - } - mLayout = new AbsoluteLayout(this); + mLayout = new RelativeLayout(this); mLayout.addView(mSurface); setContentView(mLayout); - - final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "My Tag"); - mWakeLock.acquire(); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + /* + * Per SDL_androidwindow.c, Android will only ever have one window, and that window + * is always flagged SDL_WINDOW_FULLSCREEN. Let's treat it as an immersive fullscreen + * window for Android UI purposes, as a result. + */ + int iFlags = + //View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | // Only available since API 19 + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_FULLSCREEN; + + getWindow().getDecorView().setSystemUiVisibility(iFlags); + + // 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("SDL", "onPause()"); + Log.v(TAG, "onPause()"); super.onPause(); - SDLActivity.handlePause(); + mNextNativeState = NativeState.PAUSED; + mIsResumedCalled = false; + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handleNativeState(); } @Override protected void onResume() { - Log.v("SDL", "onResume()"); + Log.v(TAG, "onResume()"); super.onResume(); - SDLActivity.handleResume(); + mNextNativeState = NativeState.RESUMED; + mIsResumedCalled = true; + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handleNativeState(); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); - Log.v("SDL", "onWindowFocusChanged(): " + hasFocus); + Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); + + if (SDLActivity.mBrokenLibraries) { + return; + } SDLActivity.mHasFocus = hasFocus; if (hasFocus) { - SDLActivity.handleResume(); + mNextNativeState = NativeState.RESUMED; + } else { + mNextNativeState = NativeState.PAUSED; } + + SDLActivity.handleNativeState(); } @Override public void onLowMemory() { - Log.v("SDL", "onLowMemory()"); + Log.v(TAG, "onLowMemory()"); super.onLowMemory(); + + if (SDLActivity.mBrokenLibraries) { + return; + } + SDLActivity.nativeLowMemory(); } @Override protected void onDestroy() { - Log.v("SDL", "onDestroy()"); + Log.v(TAG, "onDestroy()"); + + if (SDLActivity.mBrokenLibraries) { + super.onDestroy(); + // Reset everything in case the user re opens the app + SDLActivity.initialize(); + return; + } + + mNextNativeState = NativeState.PAUSED; + SDLActivity.handleNativeState(); + // Send a quit message to the application SDLActivity.mExitCalledFromJava = true; SDLActivity.nativeQuit(); @@ -146,62 +317,87 @@ public class SDLActivity extends Activity { try { SDLActivity.mSDLThread.join(); } catch(Exception e) { - Log.v("SDL", "Problem stopping thread: " + e); + Log.v(TAG, "Problem stopping thread: " + e); } SDLActivity.mSDLThread = null; - //Log.v("SDL", "Finished waiting for SDL thread"); + //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 */ + keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */ + keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */ ) { 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.enableSensor(Sensor.TYPE_ACCELEROMETER, false); + /* Transition to next state */ + public static void handleNativeState() { + + if (mNextNativeState == mCurrentNativeState) { + // Already in same state, discard. + return; + } + + // Try a transition to init state + if (mNextNativeState == NativeState.INIT) { + + mCurrentNativeState = mNextNativeState; + return; + } + + // Try a transition to paused state + if (mNextNativeState == NativeState.PAUSED) { + nativePause(); + if (mSurface != null) + mSurface.handlePause(); + mCurrentNativeState = mNextNativeState; + return; + } + + // Try a transition to resumed state + if (mNextNativeState == NativeState.RESUMED) { + if (mIsSurfaceReady && mHasFocus && mIsResumedCalled) { + if (mSDLThread == null) { + // This is the entry point to the C app. + // Start up the C app thread and enable sensor input for the first time + // FIXME: Why aren't we enabling sensor input at start? + + mSDLThread = new Thread(new SDLMain(), "SDLThread"); + mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); + mSDLThread.start(); + } + + nativeResume(); + mSurface.handleResume(); + mCurrentNativeState = mNextNativeState; + } } } - /** 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.enableSensor(Sensor.TYPE_ACCELEROMETER, true); - } - } - /* The native thread has finished */ public static void handleNativeExit() { SDLActivity.mSDLThread = null; - if ( null != mSingleton ) - mSingleton.finish(); + mSingleton.finish(); } @@ -209,6 +405,7 @@ public class SDLActivity extends Activity { 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; @@ -232,7 +429,7 @@ public class SDLActivity extends Activity { protected static class SDLCommandHandler extends Handler { @Override public void handleMessage(Message msg) { - Context context = getContext(); + Context context = SDL.getContext(); if (context == null) { Log.e(TAG, "error handling message, getContext() returned null"); return; @@ -247,13 +444,31 @@ public class SDLActivity extends Activity { break; case COMMAND_TEXTEDIT_HIDE: if (mTextEdit != null) { - mTextEdit.setVisibility(View.GONE); + // 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); + + mScreenKeyboardShown = false; } break; - + case COMMAND_SET_KEEP_SCREEN_ON: + { + if (context instanceof Activity) { + 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); @@ -274,76 +489,153 @@ public class SDLActivity extends Activity { } // C functions we call - public static native void nativeInit(); + public static native int nativeSetupJNI(); + public static native int nativeRunMain(String library, String function, 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 onNativeResize(int x, int y, int format); - 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 onNativeDropFile(String filename); + public static native void onNativeResize(int x, int y, int format, float rate); 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, + int action, float x, float y, float p); public static native void onNativeAccel(float x, float y, float z); + public static native void onNativeClipboardChanged(); public static native void onNativeSurfaceChanged(); public static native void onNativeSurfaceDestroyed(); - public static native void nativeFlipBuffers(); - 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 void flipBuffers() { - SDLActivity.nativeFlipBuffers(); - } + public static native String nativeGetHint(String name); + public static native void nativeSetenv(String name, String value); + /** + * 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); } - public static boolean sendMessage(int command, int param) { - return mSingleton.sendCommand(command, Integer.valueOf(param)); - } - - public static Context getContext() { - return mSingleton; - } - /** - * @return result of getSystemService(name) but executed on UI thread. + * This method is called by SDL using JNI. + * This is a static method for JNI convenience, it calls a non-static method + * so that is can be overridden */ - 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(); + public static void setOrientation(int w, int h, boolean resizable, String hint) + { + if (mSingleton != null) { + mSingleton.setOrientationBis(w, h, resizable, hint); + } + } + + /** + * This can be overridden + */ + public void setOrientationBis(int w, int h, boolean resizable, String hint) + { + int orientation = -1; + + if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + } else if (hint.contains("LandscapeRight")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + } else if (hint.contains("LandscapeLeft")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + } else if (hint.contains("Portrait") && hint.contains("PortraitUpsideDown")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + } else if (hint.contains("Portrait")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + } else if (hint.contains("PortraitUpsideDown")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; + } + + /* no valid hint */ + if (orientation == -1) { + if (resizable) { + /* no fixed orientation */ + } else { + if (w > h) { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + } else { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; } } } - return results[0]; + + Log.v("SDL", "setOrientation() orientation=" + orientation + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); + if (orientation != -1) { + mSingleton.setRequestedOrientation(orientation); + } + } + + + /** + * This method is called by SDL using JNI. + */ + public static boolean isScreenKeyboardShown() + { + if (mTextEdit == null) { + return false; + } + + if (!mScreenKeyboardShown) { + return false; + } + + InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.isAcceptingText(); + + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean sendMessage(int command, int param) { + if (mSingleton == null) { + return false; + } + return mSingleton.sendCommand(command, Integer.valueOf(param)); + } + + /** + * This method is called by SDL using JNI. + */ + public static Context getContext() { + return SDL.getContext(); + } + + public static DisplayMetrics getDisplayDPI() { + return getContext().getResources().getDisplayMetrics(); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean getManifestEnvironmentVariables() { + try { + ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); + Bundle bundle = applicationInfo.metaData; + if (bundle == null) { + return false; + } + String prefix = "SDL_ENV."; + final int trimLength = prefix.length(); + for (String key : bundle.keySet()) { + if (key.startsWith(prefix)) { + String name = key.substring(trimLength); + String value = bundle.get(key).toString(); + nativeSetenv(name, value); + } + } + /* environment variables set! */ + return true; + } catch (Exception e) { + Log.v("SDL", "exception " + e.toString()); + } + return false; } static class ShowTextInputTask implements Runnable { @@ -365,11 +657,12 @@ public class SDLActivity extends Activity { @Override public void run() { - AbsoluteLayout.LayoutParams params = new AbsoluteLayout.LayoutParams( - w, h + HEIGHT_PADDING, x, y); + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); + params.leftMargin = x; + params.topMargin = y; if (mTextEdit == null) { - mTextEdit = new DummyEdit(getContext()); + mTextEdit = new DummyEdit(SDL.getContext()); mLayout.addView(mTextEdit, params); } else { @@ -379,101 +672,47 @@ public class SDLActivity extends Activity { mTextEdit.setVisibility(View.VISIBLE); mTextEdit.requestFocus(); - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mTextEdit, 0); + + mScreenKeyboardShown = true; } } + /** + * 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)); } - + + public static boolean isTextInputEvent(KeyEvent event) { + + // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT + if (android.os.Build.VERSION.SDK_INT >= 11) { + if (event.isCtrlPressed()) { + return false; + } + } + + return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; + } + + /** + * This method is called by SDL using JNI. + */ public static Surface getNativeSurface() { + if (SDLActivity.mSurface == null) { + return null; + } return SDLActivity.mSurface.getNativeSurface(); } - // Audio - public static int audioInit(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("SDL", "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("SDL", "Failed during initialization of Audio Track"); - mAudioTrack = null; - return -1; - } - - mAudioTrack.play(); - } - - Log.v("SDL", "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; - } - - 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("SDL", "SDL audio: error return from write(short)"); - return; - } - } - } - - 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("SDL", "SDL audio: error return from write(byte)"); - return; - } - } - } - - public static void audioQuit() { - if (mAudioTrack != null) { - mAudioTrack.stop(); - mAudioTrack = 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) { @@ -488,41 +727,336 @@ public class SDLActivity extends Activity { } 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); - } - - public static void pollInputDevices() { - if (SDLActivity.mSDLThread != null) { - mJoystickHandler.pollInputDevices(); + + // APK expansion files support + + /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */ + private static Object expansionFile; + + /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */ + private static 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 static 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, SDL.getContext(), 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