1 package org.libsdl.app;
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.util.ArrayList;
6 import java.util.Arrays;
7 import java.util.Collections;
8 import java.util.Comparator;
10 import java.lang.reflect.Method;
13 import android.content.*;
14 import android.text.InputType;
15 import android.view.*;
16 import android.view.inputmethod.BaseInputConnection;
17 import android.view.inputmethod.EditorInfo;
18 import android.view.inputmethod.InputConnection;
19 import android.view.inputmethod.InputMethodManager;
20 import android.widget.AbsoluteLayout;
21 import android.widget.Button;
22 import android.widget.LinearLayout;
23 import android.widget.TextView;
25 import android.util.Log;
26 import android.util.SparseArray;
27 import android.graphics.*;
28 import android.graphics.drawable.Drawable;
29 import android.media.*;
30 import android.hardware.*;
31 import android.content.pm.ActivityInfo;
36 public class SDLActivity extends Activity {
37 private static final String TAG = "SDL";
39 // Keep track of the paused state
40 public static boolean mIsPaused, mIsSurfaceReady, mHasFocus;
41 public static boolean mExitCalledFromJava;
43 /** If shared libraries (e.g. SDL or the native application) could not be loaded. */
44 public static boolean mBrokenLibraries;
46 // If we want to separate mouse and touch events.
47 // This is only toggled in native code when a hint is set!
48 public static boolean mSeparateMouseAndTouch;
51 protected static SDLActivity mSingleton;
52 protected static SDLSurface mSurface;
53 protected static View mTextEdit;
54 protected static ViewGroup mLayout;
55 protected static SDLJoystickHandler mJoystickHandler;
57 // This is what SDL runs in. It invokes SDL_main(), eventually
58 protected static Thread mSDLThread;
61 protected static AudioTrack mAudioTrack;
64 * This method is called by SDL before loading the native shared libraries.
65 * It can be overridden to provide names of shared libraries to be loaded.
66 * The default implementation returns the defaults. It never returns null.
67 * An array returned by a new implementation must at least contain "SDL2".
68 * Also keep in mind that the order the libraries are loaded may matter.
69 * @return names of shared libraries to be loaded (e.g. "SDL2", "main").
71 protected String[] getLibraries() {
83 public void loadLibraries() {
84 for (String lib : getLibraries()) {
85 System.loadLibrary(lib);
90 * This method is called by SDL before starting the native application thread.
91 * It can be overridden to provide the arguments after the application name.
92 * The default implementation returns an empty array. It never returns null.
93 * @return arguments for the native application.
95 protected String[] getArguments() {
99 public static void initialize() {
100 // The static nature of the singleton and Android quirkyness force us to initialize everything here
101 // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
106 mJoystickHandler = null;
109 mExitCalledFromJava = false;
110 mBrokenLibraries = false;
112 mIsSurfaceReady = false;
118 protected void onCreate(Bundle savedInstanceState) {
119 Log.v(TAG, "Device: " + android.os.Build.DEVICE);
120 Log.v(TAG, "Model: " + android.os.Build.MODEL);
121 Log.v(TAG, "onCreate(): " + mSingleton);
122 super.onCreate(savedInstanceState);
124 SDLActivity.initialize();
125 // So we can call stuff from static callbacks
128 // Load shared libraries
129 String errorMsgBrokenLib = "";
132 } catch(UnsatisfiedLinkError e) {
133 System.err.println(e.getMessage());
134 mBrokenLibraries = true;
135 errorMsgBrokenLib = e.getMessage();
136 } catch(Exception e) {
137 System.err.println(e.getMessage());
138 mBrokenLibraries = true;
139 errorMsgBrokenLib = e.getMessage();
142 if (mBrokenLibraries)
144 AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this);
145 dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall."
146 + System.getProperty("line.separator")
147 + System.getProperty("line.separator")
148 + "Error: " + errorMsgBrokenLib);
149 dlgAlert.setTitle("SDL Error");
150 dlgAlert.setPositiveButton("Exit",
151 new DialogInterface.OnClickListener() {
153 public void onClick(DialogInterface dialog,int id) {
154 // if this button is clicked, close current activity
155 SDLActivity.mSingleton.finish();
158 dlgAlert.setCancelable(false);
159 dlgAlert.create().show();
164 // Set up the surface
165 mSurface = new SDLSurface(getApplication());
167 if(Build.VERSION.SDK_INT >= 12) {
168 mJoystickHandler = new SDLJoystickHandler_API12();
171 mJoystickHandler = new SDLJoystickHandler();
174 mLayout = new AbsoluteLayout(this);
175 mLayout.addView(mSurface);
177 setContentView(mLayout);
179 // Get filename from "Open with" of another application
180 Intent intent = getIntent();
182 if (intent != null && intent.getData() != null) {
183 String filename = intent.getData().getPath();
184 if (filename != null) {
185 Log.v(TAG, "Got filename: " + filename);
186 SDLActivity.onNativeDropFile(filename);
193 protected void onPause() {
194 Log.v(TAG, "onPause()");
197 if (SDLActivity.mBrokenLibraries) {
201 SDLActivity.handlePause();
205 protected void onResume() {
206 Log.v(TAG, "onResume()");
209 if (SDLActivity.mBrokenLibraries) {
213 SDLActivity.handleResume();
218 public void onWindowFocusChanged(boolean hasFocus) {
219 super.onWindowFocusChanged(hasFocus);
220 Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
222 if (SDLActivity.mBrokenLibraries) {
226 SDLActivity.mHasFocus = hasFocus;
228 SDLActivity.handleResume();
233 public void onLowMemory() {
234 Log.v(TAG, "onLowMemory()");
237 if (SDLActivity.mBrokenLibraries) {
241 SDLActivity.nativeLowMemory();
245 protected void onDestroy() {
246 Log.v(TAG, "onDestroy()");
248 if (SDLActivity.mBrokenLibraries) {
250 // Reset everything in case the user re opens the app
251 SDLActivity.initialize();
255 // Send a quit message to the application
256 SDLActivity.mExitCalledFromJava = true;
257 SDLActivity.nativeQuit();
259 // Now wait for the SDL thread to quit
260 if (SDLActivity.mSDLThread != null) {
262 SDLActivity.mSDLThread.join();
263 } catch(Exception e) {
264 Log.v(TAG, "Problem stopping thread: " + e);
266 SDLActivity.mSDLThread = null;
268 //Log.v(TAG, "Finished waiting for SDL thread");
272 // Reset everything in case the user re opens the app
273 SDLActivity.initialize();
277 public boolean dispatchKeyEvent(KeyEvent event) {
279 if (SDLActivity.mBrokenLibraries) {
283 int keyCode = event.getKeyCode();
284 // Ignore certain special keys so they're handled by Android
285 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
286 keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
287 keyCode == KeyEvent.KEYCODE_CAMERA ||
288 keyCode == 168 || /* API 11: KeyEvent.KEYCODE_ZOOM_IN */
289 keyCode == 169 /* API 11: KeyEvent.KEYCODE_ZOOM_OUT */
293 return super.dispatchKeyEvent(event);
296 /** Called by onPause or surfaceDestroyed. Even if surfaceDestroyed
297 * is the first to be called, mIsSurfaceReady should still be set
298 * to 'true' during the call to onPause (in a usual scenario).
300 public static void handlePause() {
301 if (!SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady) {
302 SDLActivity.mIsPaused = true;
303 SDLActivity.nativePause();
304 mSurface.handlePause();
308 /** Called by onResume or surfaceCreated. An actual resume should be done only when the surface is ready.
309 * Note: Some Android variants may send multiple surfaceChanged events, so we don't need to resume
310 * every time we get one of those events, only if it comes after surfaceDestroyed
312 public static void handleResume() {
313 if (SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady && SDLActivity.mHasFocus) {
314 SDLActivity.mIsPaused = false;
315 SDLActivity.nativeResume();
316 mSurface.handleResume();
320 /* The native thread has finished */
321 public static void handleNativeExit() {
322 SDLActivity.mSDLThread = null;
327 // Messages from the SDLMain thread
328 static final int COMMAND_CHANGE_TITLE = 1;
329 static final int COMMAND_UNUSED = 2;
330 static final int COMMAND_TEXTEDIT_HIDE = 3;
331 static final int COMMAND_SET_KEEP_SCREEN_ON = 5;
333 protected static final int COMMAND_USER = 0x8000;
336 * This method is called by SDL if SDL did not handle a message itself.
337 * This happens if a received message contains an unsupported command.
338 * Method can be overwritten to handle Messages in a different class.
339 * @param command the command of the message.
340 * @param param the parameter of the message. May be null.
341 * @return if the message was handled in overridden method.
343 protected boolean onUnhandledMessage(int command, Object param) {
348 * A Handler class for Messages from native SDL applications.
349 * It uses current Activities as target (e.g. for the title).
350 * static to prevent implicit references to enclosing object.
352 protected static class SDLCommandHandler extends Handler {
354 public void handleMessage(Message msg) {
355 Context context = getContext();
356 if (context == null) {
357 Log.e(TAG, "error handling message, getContext() returned null");
361 case COMMAND_CHANGE_TITLE:
362 if (context instanceof Activity) {
363 ((Activity) context).setTitle((String)msg.obj);
365 Log.e(TAG, "error handling message, getContext() returned no Activity");
368 case COMMAND_TEXTEDIT_HIDE:
369 if (mTextEdit != null) {
370 mTextEdit.setVisibility(View.GONE);
372 InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
373 imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0);
376 case COMMAND_SET_KEEP_SCREEN_ON:
378 Window window = ((Activity) context).getWindow();
379 if (window != null) {
380 if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) {
381 window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
383 window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
389 if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) {
390 Log.e(TAG, "error handling message, command is " + msg.arg1);
396 // Handler for the messages
397 Handler commandHandler = new SDLCommandHandler();
399 // Send a message from the SDLMain thread
400 boolean sendCommand(int command, Object data) {
401 Message msg = commandHandler.obtainMessage();
404 return commandHandler.sendMessage(msg);
407 // C functions we call
408 public static native int nativeInit(Object arguments);
409 public static native void nativeLowMemory();
410 public static native void nativeQuit();
411 public static native void nativePause();
412 public static native void nativeResume();
413 public static native void onNativeDropFile(String filename);
414 public static native void onNativeResize(int x, int y, int format, float rate);
415 public static native int onNativePadDown(int device_id, int keycode);
416 public static native int onNativePadUp(int device_id, int keycode);
417 public static native void onNativeJoy(int device_id, int axis,
419 public static native void onNativeHat(int device_id, int hat_id,
421 public static native void onNativeKeyDown(int keycode);
422 public static native void onNativeKeyUp(int keycode);
423 public static native void onNativeKeyboardFocusLost();
424 public static native void onNativeMouse(int button, int action, float x, float y);
425 public static native void onNativeTouch(int touchDevId, int pointerFingerId,
428 public static native void onNativeAccel(float x, float y, float z);
429 public static native void onNativeSurfaceChanged();
430 public static native void onNativeSurfaceDestroyed();
431 public static native int nativeAddJoystick(int device_id, String name,
432 int is_accelerometer, int nbuttons,
433 int naxes, int nhats, int nballs);
434 public static native int nativeRemoveJoystick(int device_id);
435 public static native String nativeGetHint(String name);
438 * This method is called by SDL using JNI.
440 public static boolean setActivityTitle(String title) {
441 // Called from SDLMain() thread and can't directly affect the view
442 return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
446 * This method is called by SDL using JNI.
448 public static boolean sendMessage(int command, int param) {
449 return mSingleton.sendCommand(command, Integer.valueOf(param));
453 * This method is called by SDL using JNI.
455 public static Context getContext() {
460 * This method is called by SDL using JNI.
461 * @return result of getSystemService(name) but executed on UI thread.
463 public Object getSystemServiceFromUiThread(final String name) {
464 final Object lock = new Object();
465 final Object[] results = new Object[2]; // array for writable variables
466 synchronized (lock) {
467 runOnUiThread(new Runnable() {
470 synchronized (lock) {
471 results[0] = getSystemService(name);
472 results[1] = Boolean.TRUE;
477 if (results[1] == null) {
480 } catch (InterruptedException ex) {
481 ex.printStackTrace();
488 static class ShowTextInputTask implements Runnable {
490 * This is used to regulate the pan&scan method to have some offset from
491 * the bottom edge of the input region and the top edge of an input
492 * method (soft keyboard)
494 static final int HEIGHT_PADDING = 15;
496 public int x, y, w, h;
498 public ShowTextInputTask(int x, int y, int w, int h) {
507 AbsoluteLayout.LayoutParams params = new AbsoluteLayout.LayoutParams(
508 w, h + HEIGHT_PADDING, x, y);
510 if (mTextEdit == null) {
511 mTextEdit = new DummyEdit(getContext());
513 mLayout.addView(mTextEdit, params);
515 mTextEdit.setLayoutParams(params);
518 mTextEdit.setVisibility(View.VISIBLE);
519 mTextEdit.requestFocus();
521 InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
522 imm.showSoftInput(mTextEdit, 0);
527 * This method is called by SDL using JNI.
529 public static boolean showTextInput(int x, int y, int w, int h) {
530 // Transfer the task to the main thread as a Runnable
531 return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h));
535 * This method is called by SDL using JNI.
537 public static Surface getNativeSurface() {
538 return SDLActivity.mSurface.getNativeSurface();
544 * This method is called by SDL using JNI.
546 public static int audioInit(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) {
547 int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO;
548 int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT;
549 int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1);
551 Log.v(TAG, "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer");
553 // Let the user pick a larger buffer if they really want -- but ye
554 // gods they probably shouldn't, the minimums are horrifyingly high
556 desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize);
558 if (mAudioTrack == null) {
559 mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
560 channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
562 // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
563 // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
564 // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
566 if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
567 Log.e(TAG, "Failed during initialization of Audio Track");
575 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");
581 * This method is called by SDL using JNI.
583 public static void audioWriteShortBuffer(short[] buffer) {
584 for (int i = 0; i < buffer.length; ) {
585 int result = mAudioTrack.write(buffer, i, buffer.length - i);
588 } else if (result == 0) {
591 } catch(InterruptedException e) {
595 Log.w(TAG, "SDL audio: error return from write(short)");
602 * This method is called by SDL using JNI.
604 public static void audioWriteByteBuffer(byte[] buffer) {
605 for (int i = 0; i < buffer.length; ) {
606 int result = mAudioTrack.write(buffer, i, buffer.length - i);
609 } else if (result == 0) {
612 } catch(InterruptedException e) {
616 Log.w(TAG, "SDL audio: error return from write(byte)");
623 * This method is called by SDL using JNI.
625 public static void audioQuit() {
626 if (mAudioTrack != null) {
635 * This method is called by SDL using JNI.
636 * @return an array which may be empty but is never null.
638 public static int[] inputGetInputDeviceIds(int sources) {
639 int[] ids = InputDevice.getDeviceIds();
640 int[] filtered = new int[ids.length];
642 for (int i = 0; i < ids.length; ++i) {
643 InputDevice device = InputDevice.getDevice(ids[i]);
644 if ((device != null) && ((device.getSources() & sources) != 0)) {
645 filtered[used++] = device.getId();
648 return Arrays.copyOf(filtered, used);
651 // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
652 public static boolean handleJoystickMotionEvent(MotionEvent event) {
653 return mJoystickHandler.handleMotionEvent(event);
657 * This method is called by SDL using JNI.
659 public static void pollInputDevices() {
660 if (SDLActivity.mSDLThread != null) {
661 mJoystickHandler.pollInputDevices();
665 // APK expansion files support
667 /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */
668 private Object expansionFile;
670 /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */
671 private Method expansionFileMethod;
674 * This method was called by SDL using JNI.
675 * @deprecated because of an incorrect name
678 public InputStream openAPKExtensionInputStream(String fileName) throws IOException {
679 return openAPKExpansionInputStream(fileName);
683 * This method is called by SDL using JNI.
684 * @return an InputStream on success or null if no expansion file was used.
685 * @throws IOException on errors. Message is set for the SDL error message.
687 public InputStream openAPKExpansionInputStream(String fileName) throws IOException {
688 // Get a ZipResourceFile representing a merger of both the main and patch files
689 if (expansionFile == null) {
690 String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION");
691 if (mainHint == null) {
692 return null; // no expansion use if no main version was set
694 String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION");
695 if (patchHint == null) {
696 return null; // no expansion use if no patch version was set
700 Integer patchVersion;
702 mainVersion = Integer.valueOf(mainHint);
703 patchVersion = Integer.valueOf(patchHint);
704 } catch (NumberFormatException ex) {
705 ex.printStackTrace();
706 throw new IOException("No valid file versions set for APK expansion files", ex);
710 // To avoid direct dependency on Google APK expansion library that is
711 // not a part of Android SDK we access it using reflection
712 expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport")
713 .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class)
714 .invoke(null, this, mainVersion, patchVersion);
716 expansionFileMethod = expansionFile.getClass()
717 .getMethod("getInputStream", String.class);
718 } catch (Exception ex) {
719 ex.printStackTrace();
720 expansionFile = null;
721 expansionFileMethod = null;
722 throw new IOException("Could not access APK expansion support library", ex);
726 // Get an input stream for a known file inside the expansion file ZIPs
727 InputStream fileStream;
729 fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName);
730 } catch (Exception ex) {
731 // calling "getInputStream" failed
732 ex.printStackTrace();
733 throw new IOException("Could not open stream from APK expansion file", ex);
736 if (fileStream == null) {
737 // calling "getInputStream" was successful but null was returned
738 throw new IOException("Could not find path in APK expansion file");
746 /** Result of current messagebox. Also used for blocking the calling thread. */
747 protected final int[] messageboxSelection = new int[1];
749 /** Id of current dialog. */
750 protected int dialogs = 0;
753 * This method is called by SDL using JNI.
754 * Shows the messagebox from UI thread and block calling thread.
755 * buttonFlags, buttonIds and buttonTexts must have same length.
756 * @param buttonFlags array containing flags for every button.
757 * @param buttonIds array containing id for every button.
758 * @param buttonTexts array containing text for every button.
759 * @param colors null for default or array of length 5 containing colors.
760 * @return button id or -1.
762 public int messageboxShowMessageBox(
765 final String message,
766 final int[] buttonFlags,
767 final int[] buttonIds,
768 final String[] buttonTexts,
769 final int[] colors) {
771 messageboxSelection[0] = -1;
775 if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) {
776 return -1; // implementation broken
779 // collect arguments for Dialog
781 final Bundle args = new Bundle();
782 args.putInt("flags", flags);
783 args.putString("title", title);
784 args.putString("message", message);
785 args.putIntArray("buttonFlags", buttonFlags);
786 args.putIntArray("buttonIds", buttonIds);
787 args.putStringArray("buttonTexts", buttonTexts);
788 args.putIntArray("colors", colors);
790 // trigger Dialog creation on UI thread
792 runOnUiThread(new Runnable() {
795 showDialog(dialogs++, args);
799 // block the calling thread
801 synchronized (messageboxSelection) {
803 messageboxSelection.wait();
804 } catch (InterruptedException ex) {
805 ex.printStackTrace();
810 // return selected value
812 return messageboxSelection[0];
816 protected Dialog onCreateDialog(int ignore, Bundle args) {
818 // TODO set values from "flags" to messagebox dialog
822 int[] colors = args.getIntArray("colors");
825 int buttonBorderColor;
826 int buttonBackgroundColor;
827 int buttonSelectedColor;
828 if (colors != null) {
830 backgroundColor = colors[++i];
831 textColor = colors[++i];
832 buttonBorderColor = colors[++i];
833 buttonBackgroundColor = colors[++i];
834 buttonSelectedColor = colors[++i];
836 backgroundColor = Color.TRANSPARENT;
837 textColor = Color.TRANSPARENT;
838 buttonBorderColor = Color.TRANSPARENT;
839 buttonBackgroundColor = Color.TRANSPARENT;
840 buttonSelectedColor = Color.TRANSPARENT;
843 // create dialog with title and a listener to wake up calling thread
845 final Dialog dialog = new Dialog(this);
846 dialog.setTitle(args.getString("title"));
847 dialog.setCancelable(false);
848 dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
850 public void onDismiss(DialogInterface unused) {
851 synchronized (messageboxSelection) {
852 messageboxSelection.notify();
859 TextView message = new TextView(this);
860 message.setGravity(Gravity.CENTER);
861 message.setText(args.getString("message"));
862 if (textColor != Color.TRANSPARENT) {
863 message.setTextColor(textColor);
868 int[] buttonFlags = args.getIntArray("buttonFlags");
869 int[] buttonIds = args.getIntArray("buttonIds");
870 String[] buttonTexts = args.getStringArray("buttonTexts");
872 final SparseArray<Button> mapping = new SparseArray<Button>();
874 LinearLayout buttons = new LinearLayout(this);
875 buttons.setOrientation(LinearLayout.HORIZONTAL);
876 buttons.setGravity(Gravity.CENTER);
877 for (int i = 0; i < buttonTexts.length; ++i) {
878 Button button = new Button(this);
879 final int id = buttonIds[i];
880 button.setOnClickListener(new View.OnClickListener() {
882 public void onClick(View v) {
883 messageboxSelection[0] = id;
887 if (buttonFlags[i] != 0) {
888 // see SDL_messagebox.h
889 if ((buttonFlags[i] & 0x00000001) != 0) {
890 mapping.put(KeyEvent.KEYCODE_ENTER, button);
892 if ((buttonFlags[i] & 0x00000002) != 0) {
893 mapping.put(111, button); /* API 11: KeyEvent.KEYCODE_ESCAPE */
896 button.setText(buttonTexts[i]);
897 if (textColor != Color.TRANSPARENT) {
898 button.setTextColor(textColor);
900 if (buttonBorderColor != Color.TRANSPARENT) {
901 // TODO set color for border of messagebox button
903 if (buttonBackgroundColor != Color.TRANSPARENT) {
904 Drawable drawable = button.getBackground();
905 if (drawable == null) {
906 // setting the color this way removes the style
907 button.setBackgroundColor(buttonBackgroundColor);
909 // setting the color this way keeps the style (gradient, padding, etc.)
910 drawable.setColorFilter(buttonBackgroundColor, PorterDuff.Mode.MULTIPLY);
913 if (buttonSelectedColor != Color.TRANSPARENT) {
914 // TODO set color for selected messagebox button
916 buttons.addView(button);
921 LinearLayout content = new LinearLayout(this);
922 content.setOrientation(LinearLayout.VERTICAL);
923 content.addView(message);
924 content.addView(buttons);
925 if (backgroundColor != Color.TRANSPARENT) {
926 content.setBackgroundColor(backgroundColor);
929 // add content to dialog and return
931 dialog.setContentView(content);
932 dialog.setOnKeyListener(new Dialog.OnKeyListener() {
934 public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) {
935 Button button = mapping.get(keyCode);
936 if (button != null) {
937 if (event.getAction() == KeyEvent.ACTION_UP) {
938 button.performClick();
940 return true; // also for ignored actions
951 Simple nativeInit() runnable
953 class SDLMain implements Runnable {
957 SDLActivity.nativeInit(SDLActivity.mSingleton.getArguments());
959 //Log.v("SDL", "SDL thread terminated");
965 SDLSurface. This is what we draw on, so we need to know when it's created
966 in order to do anything useful.
968 Because of this, that's where we set up the SDL thread
970 class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
971 View.OnKeyListener, View.OnTouchListener, SensorEventListener {
974 protected static SensorManager mSensorManager;
975 protected static Display mDisplay;
977 // Keep track of the surface size to normalize touch events
978 protected static float mWidth, mHeight;
981 public SDLSurface(Context context) {
983 getHolder().addCallback(this);
986 setFocusableInTouchMode(true);
988 setOnKeyListener(this);
989 setOnTouchListener(this);
991 mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
992 mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
994 if(Build.VERSION.SDK_INT >= 12) {
995 setOnGenericMotionListener(new SDLGenericMotionListener_API12());
998 // Some arbitrary defaults to avoid a potential division by zero
1003 public void handlePause() {
1004 enableSensor(Sensor.TYPE_ACCELEROMETER, false);
1007 public void handleResume() {
1009 setFocusableInTouchMode(true);
1011 setOnKeyListener(this);
1012 setOnTouchListener(this);
1013 enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1016 public Surface getNativeSurface() {
1017 return getHolder().getSurface();
1020 // Called when we have a valid drawing surface
1022 public void surfaceCreated(SurfaceHolder holder) {
1023 Log.v("SDL", "surfaceCreated()");
1024 holder.setType(SurfaceHolder.SURFACE_TYPE_GPU);
1027 // Called when we lose the surface
1029 public void surfaceDestroyed(SurfaceHolder holder) {
1030 Log.v("SDL", "surfaceDestroyed()");
1031 // Call this *before* setting mIsSurfaceReady to 'false'
1032 SDLActivity.handlePause();
1033 SDLActivity.mIsSurfaceReady = false;
1034 SDLActivity.onNativeSurfaceDestroyed();
1037 // Called when the surface is resized
1039 public void surfaceChanged(SurfaceHolder holder,
1040 int format, int width, int height) {
1041 Log.v("SDL", "surfaceChanged()");
1043 int sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 by default
1045 case PixelFormat.A_8:
1046 Log.v("SDL", "pixel format A_8");
1048 case PixelFormat.LA_88:
1049 Log.v("SDL", "pixel format LA_88");
1051 case PixelFormat.L_8:
1052 Log.v("SDL", "pixel format L_8");
1054 case PixelFormat.RGBA_4444:
1055 Log.v("SDL", "pixel format RGBA_4444");
1056 sdlFormat = 0x15421002; // SDL_PIXELFORMAT_RGBA4444
1058 case PixelFormat.RGBA_5551:
1059 Log.v("SDL", "pixel format RGBA_5551");
1060 sdlFormat = 0x15441002; // SDL_PIXELFORMAT_RGBA5551
1062 case PixelFormat.RGBA_8888:
1063 Log.v("SDL", "pixel format RGBA_8888");
1064 sdlFormat = 0x16462004; // SDL_PIXELFORMAT_RGBA8888
1066 case PixelFormat.RGBX_8888:
1067 Log.v("SDL", "pixel format RGBX_8888");
1068 sdlFormat = 0x16261804; // SDL_PIXELFORMAT_RGBX8888
1070 case PixelFormat.RGB_332:
1071 Log.v("SDL", "pixel format RGB_332");
1072 sdlFormat = 0x14110801; // SDL_PIXELFORMAT_RGB332
1074 case PixelFormat.RGB_565:
1075 Log.v("SDL", "pixel format RGB_565");
1076 sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565
1078 case PixelFormat.RGB_888:
1079 Log.v("SDL", "pixel format RGB_888");
1080 // Not sure this is right, maybe SDL_PIXELFORMAT_RGB24 instead?
1081 sdlFormat = 0x16161804; // SDL_PIXELFORMAT_RGB888
1084 Log.v("SDL", "pixel format unknown " + format);
1090 SDLActivity.onNativeResize(width, height, sdlFormat, mDisplay.getRefreshRate());
1091 Log.v("SDL", "Window size: " + width + "x" + height);
1094 boolean skip = false;
1095 int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
1097 if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
1101 else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
1103 if (mWidth > mHeight) {
1106 } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
1107 if (mWidth < mHeight) {
1112 // Special Patch for Square Resolution: Black Berry Passport
1114 double min = Math.min(mWidth, mHeight);
1115 double max = Math.max(mWidth, mHeight);
1117 if (max / min < 1.20) {
1118 Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
1124 Log.v("SDL", "Skip .. Surface is not ready.");
1129 // Set mIsSurfaceReady to 'true' *before* making a call to handleResume
1130 SDLActivity.mIsSurfaceReady = true;
1131 SDLActivity.onNativeSurfaceChanged();
1134 if (SDLActivity.mSDLThread == null) {
1135 // This is the entry point to the C app.
1136 // Start up the C app thread and enable sensor input for the first time
1138 final Thread sdlThread = new Thread(new SDLMain(), "SDLThread");
1139 enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1142 // Set up a listener thread to catch when the native thread ends
1143 SDLActivity.mSDLThread = new Thread(new Runnable(){
1149 catch(Exception e){}
1151 // Native thread has finished
1152 if (! SDLActivity.mExitCalledFromJava) {
1153 SDLActivity.handleNativeExit();
1157 }, "SDLThreadListener");
1158 SDLActivity.mSDLThread.start();
1161 if (SDLActivity.mHasFocus) {
1162 SDLActivity.handleResume();
1168 public boolean onKey(View v, int keyCode, KeyEvent event) {
1169 // Dispatch the different events depending on where they come from
1170 // Some SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD
1171 // So, we try to process them as DPAD or GAMEPAD events first, if that fails we try them as KEYBOARD
1173 if ( (event.getSource() & InputDevice.SOURCE_GAMEPAD) != 0 ||
1174 (event.getSource() & InputDevice.SOURCE_DPAD) != 0 ) {
1175 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1176 if (SDLActivity.onNativePadDown(event.getDeviceId(), keyCode) == 0) {
1179 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1180 if (SDLActivity.onNativePadUp(event.getDeviceId(), keyCode) == 0) {
1186 if( (event.getSource() & InputDevice.SOURCE_KEYBOARD) != 0) {
1187 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1188 //Log.v("SDL", "key down: " + keyCode);
1189 SDLActivity.onNativeKeyDown(keyCode);
1192 else if (event.getAction() == KeyEvent.ACTION_UP) {
1193 //Log.v("SDL", "key up: " + keyCode);
1194 SDLActivity.onNativeKeyUp(keyCode);
1204 public boolean onTouch(View v, MotionEvent event) {
1205 /* Ref: http://developer.android.com/training/gestures/multi.html */
1206 final int touchDevId = event.getDeviceId();
1207 final int pointerCount = event.getPointerCount();
1208 int action = event.getActionMasked();
1209 int pointerFingerId;
1214 // !!! FIXME: dump this SDK check after 2.0.4 ships and require API14.
1215 if (event.getSource() == InputDevice.SOURCE_MOUSE && SDLActivity.mSeparateMouseAndTouch) {
1216 if (Build.VERSION.SDK_INT < 14) {
1217 mouseButton = 1; // For Android==12 all mouse buttons are the left button
1220 mouseButton = (Integer) event.getClass().getMethod("getButtonState").invoke(event);
1221 } catch(Exception e) {
1222 mouseButton = 1; // oh well.
1225 SDLActivity.onNativeMouse(mouseButton, action, event.getX(0), event.getY(0));
1228 case MotionEvent.ACTION_MOVE:
1229 for (i = 0; i < pointerCount; i++) {
1230 pointerFingerId = event.getPointerId(i);
1231 x = event.getX(i) / mWidth;
1232 y = event.getY(i) / mHeight;
1233 p = event.getPressure(i);
1235 // may be larger than 1.0f on some devices
1236 // see the documentation of getPressure(i)
1239 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1243 case MotionEvent.ACTION_UP:
1244 case MotionEvent.ACTION_DOWN:
1245 // Primary pointer up/down, the index is always zero
1247 case MotionEvent.ACTION_POINTER_UP:
1248 case MotionEvent.ACTION_POINTER_DOWN:
1249 // Non primary pointer up/down
1251 i = event.getActionIndex();
1254 pointerFingerId = event.getPointerId(i);
1255 x = event.getX(i) / mWidth;
1256 y = event.getY(i) / mHeight;
1257 p = event.getPressure(i);
1259 // may be larger than 1.0f on some devices
1260 // see the documentation of getPressure(i)
1263 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1266 case MotionEvent.ACTION_CANCEL:
1267 for (i = 0; i < pointerCount; i++) {
1268 pointerFingerId = event.getPointerId(i);
1269 x = event.getX(i) / mWidth;
1270 y = event.getY(i) / mHeight;
1271 p = event.getPressure(i);
1273 // may be larger than 1.0f on some devices
1274 // see the documentation of getPressure(i)
1277 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
1290 public void enableSensor(int sensortype, boolean enabled) {
1291 // TODO: This uses getDefaultSensor - what if we have >1 accels?
1293 mSensorManager.registerListener(this,
1294 mSensorManager.getDefaultSensor(sensortype),
1295 SensorManager.SENSOR_DELAY_GAME, null);
1297 mSensorManager.unregisterListener(this,
1298 mSensorManager.getDefaultSensor(sensortype));
1303 public void onAccuracyChanged(Sensor sensor, int accuracy) {
1308 public void onSensorChanged(SensorEvent event) {
1309 if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
1311 switch (mDisplay.getRotation()) {
1312 case Surface.ROTATION_90:
1313 x = -event.values[1];
1314 y = event.values[0];
1316 case Surface.ROTATION_270:
1317 x = event.values[1];
1318 y = -event.values[0];
1320 case Surface.ROTATION_180:
1321 x = -event.values[1];
1322 y = -event.values[0];
1325 x = event.values[0];
1326 y = event.values[1];
1329 SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
1330 y / SensorManager.GRAVITY_EARTH,
1331 event.values[2] / SensorManager.GRAVITY_EARTH - 1);
1336 /* This is a fake invisible editor view that receives the input and defines the
1339 class DummyEdit extends View implements View.OnKeyListener {
1342 public DummyEdit(Context context) {
1344 setFocusableInTouchMode(true);
1346 setOnKeyListener(this);
1350 public boolean onCheckIsTextEditor() {
1355 public boolean onKey(View v, int keyCode, KeyEvent event) {
1357 // This handles the hardware keyboard input
1358 if (event.isPrintingKey()) {
1359 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1360 ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
1365 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1366 SDLActivity.onNativeKeyDown(keyCode);
1368 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1369 SDLActivity.onNativeKeyUp(keyCode);
1378 public boolean onKeyPreIme (int keyCode, KeyEvent event) {
1379 // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
1380 // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
1381 // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
1382 // FIXME: A more effective solution would be to change our Layout from AbsoluteLayout to Relative or Linear
1383 // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
1384 // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
1385 if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
1386 if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
1387 SDLActivity.onNativeKeyboardFocusLost();
1390 return super.onKeyPreIme(keyCode, event);
1394 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
1395 ic = new SDLInputConnection(this, true);
1397 outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
1398 outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
1399 | 33554432 /* API 11: EditorInfo.IME_FLAG_NO_FULLSCREEN */;
1405 class SDLInputConnection extends BaseInputConnection {
1407 public SDLInputConnection(View targetView, boolean fullEditor) {
1408 super(targetView, fullEditor);
1413 public boolean sendKeyEvent(KeyEvent event) {
1416 * This handles the keycodes from soft keyboard (and IME-translated
1417 * input from hardkeyboard)
1419 int keyCode = event.getKeyCode();
1420 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1421 if (event.isPrintingKey()) {
1422 commitText(String.valueOf((char) event.getUnicodeChar()), 1);
1424 SDLActivity.onNativeKeyDown(keyCode);
1426 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1428 SDLActivity.onNativeKeyUp(keyCode);
1431 return super.sendKeyEvent(event);
1435 public boolean commitText(CharSequence text, int newCursorPosition) {
1437 nativeCommitText(text.toString(), newCursorPosition);
1439 return super.commitText(text, newCursorPosition);
1443 public boolean setComposingText(CharSequence text, int newCursorPosition) {
1445 nativeSetComposingText(text.toString(), newCursorPosition);
1447 return super.setComposingText(text, newCursorPosition);
1450 public native void nativeCommitText(String text, int newCursorPosition);
1452 public native void nativeSetComposingText(String text, int newCursorPosition);
1455 public boolean deleteSurroundingText(int beforeLength, int afterLength) {
1456 // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection
1457 if (beforeLength == 1 && afterLength == 0) {
1459 return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
1460 && super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
1463 return super.deleteSurroundingText(beforeLength, afterLength);
1467 /* A null joystick handler for API level < 12 devices (the accelerometer is handled separately) */
1468 class SDLJoystickHandler {
1471 * Handles given MotionEvent.
1472 * @param event the event to be handled.
1473 * @return if given event was processed.
1475 public boolean handleMotionEvent(MotionEvent event) {
1480 * Handles adding and removing of input devices.
1482 public void pollInputDevices() {
1486 /* Actual joystick functionality available for API >= 12 devices */
1487 class SDLJoystickHandler_API12 extends SDLJoystickHandler {
1489 static class SDLJoystick {
1490 public int device_id;
1492 public ArrayList<InputDevice.MotionRange> axes;
1493 public ArrayList<InputDevice.MotionRange> hats;
1495 static class RangeComparator implements Comparator<InputDevice.MotionRange> {
1497 public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
1498 return arg0.getAxis() - arg1.getAxis();
1502 private ArrayList<SDLJoystick> mJoysticks;
1504 public SDLJoystickHandler_API12() {
1506 mJoysticks = new ArrayList<SDLJoystick>();
1510 public void pollInputDevices() {
1511 int[] deviceIds = InputDevice.getDeviceIds();
1512 // It helps processing the device ids in reverse order
1513 // For example, in the case of the XBox 360 wireless dongle,
1514 // so the first controller seen by SDL matches what the receiver
1515 // considers to be the first controller
1517 for(int i=deviceIds.length-1; i>-1; i--) {
1518 SDLJoystick joystick = getJoystick(deviceIds[i]);
1519 if (joystick == null) {
1520 joystick = new SDLJoystick();
1521 InputDevice joystickDevice = InputDevice.getDevice(deviceIds[i]);
1524 (joystickDevice.getSources() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0
1526 (joystickDevice.getSources() & InputDevice.SOURCE_CLASS_BUTTON) != 0
1529 joystick.device_id = deviceIds[i];
1530 joystick.name = joystickDevice.getName();
1531 joystick.axes = new ArrayList<InputDevice.MotionRange>();
1532 joystick.hats = new ArrayList<InputDevice.MotionRange>();
1534 List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
1535 Collections.sort(ranges, new RangeComparator());
1536 for (InputDevice.MotionRange range : ranges ) {
1537 if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ) {
1538 if (range.getAxis() == MotionEvent.AXIS_HAT_X ||
1539 range.getAxis() == MotionEvent.AXIS_HAT_Y) {
1540 joystick.hats.add(range);
1543 joystick.axes.add(range);
1548 mJoysticks.add(joystick);
1549 SDLActivity.nativeAddJoystick(joystick.device_id, joystick.name, 0, -1,
1550 joystick.axes.size(), joystick.hats.size()/2, 0);
1555 /* Check removed devices */
1556 ArrayList<Integer> removedDevices = new ArrayList<Integer>();
1557 for(int i=0; i < mJoysticks.size(); i++) {
1558 int device_id = mJoysticks.get(i).device_id;
1560 for (j=0; j < deviceIds.length; j++) {
1561 if (device_id == deviceIds[j]) break;
1563 if (j == deviceIds.length) {
1564 removedDevices.add(Integer.valueOf(device_id));
1568 for(int i=0; i < removedDevices.size(); i++) {
1569 int device_id = removedDevices.get(i).intValue();
1570 SDLActivity.nativeRemoveJoystick(device_id);
1571 for (int j=0; j < mJoysticks.size(); j++) {
1572 if (mJoysticks.get(j).device_id == device_id) {
1573 mJoysticks.remove(j);
1580 protected SDLJoystick getJoystick(int device_id) {
1581 for(int i=0; i < mJoysticks.size(); i++) {
1582 if (mJoysticks.get(i).device_id == device_id) {
1583 return mJoysticks.get(i);
1590 public boolean handleMotionEvent(MotionEvent event) {
1591 if ( (event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0) {
1592 int actionPointerIndex = event.getActionIndex();
1593 int action = event.getActionMasked();
1595 case MotionEvent.ACTION_MOVE:
1596 SDLJoystick joystick = getJoystick(event.getDeviceId());
1597 if ( joystick != null ) {
1598 for (int i = 0; i < joystick.axes.size(); i++) {
1599 InputDevice.MotionRange range = joystick.axes.get(i);
1600 /* Normalize the value to -1...1 */
1601 float value = ( event.getAxisValue( range.getAxis(), actionPointerIndex) - range.getMin() ) / range.getRange() * 2.0f - 1.0f;
1602 SDLActivity.onNativeJoy(joystick.device_id, i, value );
1604 for (int i = 0; i < joystick.hats.size(); i+=2) {
1605 int hatX = Math.round(event.getAxisValue( joystick.hats.get(i).getAxis(), actionPointerIndex ) );
1606 int hatY = Math.round(event.getAxisValue( joystick.hats.get(i+1).getAxis(), actionPointerIndex ) );
1607 SDLActivity.onNativeHat(joystick.device_id, i/2, hatX, hatY );
1619 class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
1620 // Generic Motion (mouse hover, joystick...) events go here
1622 public boolean onGenericMotion(View v, MotionEvent event) {
1626 switch ( event.getSource() ) {
1627 case InputDevice.SOURCE_JOYSTICK:
1628 case InputDevice.SOURCE_GAMEPAD:
1629 case InputDevice.SOURCE_DPAD:
1630 SDLActivity.handleJoystickMotionEvent(event);
1633 case InputDevice.SOURCE_MOUSE:
1634 action = event.getActionMasked();
1636 case MotionEvent.ACTION_SCROLL:
1637 x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
1638 y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
1639 SDLActivity.onNativeMouse(0, action, x, y);
1642 case MotionEvent.ACTION_HOVER_MOVE:
1646 SDLActivity.onNativeMouse(0, action, x, y);
1658 // Event was not managed