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"; 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 ViewGroup mLayout; protected static SDLJoystickHandler mJoystickHandler; protected static SDLHapticHandler mHapticHandler; // 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; mHapticHandler = null; mSDLThread = null; mAudioTrack = null; mAudioRecord = null; mExitCalledFromJava = false; mBrokenLibraries = false; mIsResumedCalled = false; mIsSurfaceReady = false; mHasFocus = true; mNextNativeState = NativeState.INIT; mCurrentNativeState = NativeState.INIT; } // 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(); } mHapticHandler = new SDLHapticHandler(); 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(); mNextNativeState = NativeState.PAUSED; mIsResumedCalled = false; if (SDLActivity.mBrokenLibraries) { return; } SDLActivity.handleNativeState(); } @Override protected void onResume() { Log.v(TAG, "onResume()"); super.onResume(); mNextNativeState = NativeState.RESUMED; mIsResumedCalled = true; if (SDLActivity.mBrokenLibraries) { return; } SDLActivity.handleNativeState(); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); if (SDLActivity.mBrokenLibraries) { return; } SDLActivity.mHasFocus = hasFocus; if (hasFocus) { mNextNativeState = NativeState.RESUMED; } else { mNextNativeState = NativeState.PAUSED; } SDLActivity.handleNativeState(); } @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; } mNextNativeState = NativeState.PAUSED; SDLActivity.handleNativeState(); // 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); } /* 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(); 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 final Thread sdlThread = new Thread(new SDLMain(), "SDLThread"); mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); sdlThread.start(); // Set up a listener thread to catch when the native thread ends mSDLThread = new Thread(new Runnable(){ @Override public void run(){ try { sdlThread.join(); } catch(Exception e){} finally{ // Native thread has finished if (! mExitCalledFromJava) { handleNativeExit(); } } } }, "SDLThreadListener"); mSDLThread.start(); } nativeResume(); mSurface.handleResume(); mCurrentNativeState = mNextNativeState; } return; } } /* 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 int nativeAddHaptic(int device_id, String name); public static native int nativeRemoveHaptic(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