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.RelativeLayout;
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;
62 protected static AudioRecord mAudioRecord;
65 * This method is called by SDL before loading the native shared libraries.
66 * It can be overridden to provide names of shared libraries to be loaded.
67 * The default implementation returns the defaults. It never returns null.
68 * An array returned by a new implementation must at least contain "SDL2".
69 * Also keep in mind that the order the libraries are loaded may matter.
70 * @return names of shared libraries to be loaded (e.g. "SDL2", "main").
72 protected String[] getLibraries() {
85 public void loadLibraries() {
86 for (String lib : getLibraries()) {
87 System.loadLibrary(lib);
92 * This method is called by SDL before starting the native application thread.
93 * It can be overridden to provide the arguments after the application name.
94 * The default implementation returns an empty array. It never returns null.
95 * @return arguments for the native application.
97 protected String[] getArguments() {
101 public static void initialize() {
102 // The static nature of the singleton and Android quirkyness force us to initialize everything here
103 // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
108 mJoystickHandler = null;
112 mExitCalledFromJava = false;
113 mBrokenLibraries = false;
115 mIsSurfaceReady = false;
121 protected void onCreate(Bundle savedInstanceState) {
122 Log.v(TAG, "Device: " + android.os.Build.DEVICE);
123 Log.v(TAG, "Model: " + android.os.Build.MODEL);
124 Log.v(TAG, "onCreate(): " + mSingleton);
125 super.onCreate(savedInstanceState);
127 SDLActivity.initialize();
128 // So we can call stuff from static callbacks
131 // Load shared libraries
132 String errorMsgBrokenLib = "";
135 } catch(UnsatisfiedLinkError e) {
136 System.err.println(e.getMessage());
137 mBrokenLibraries = true;
138 errorMsgBrokenLib = e.getMessage();
139 } catch(Exception e) {
140 System.err.println(e.getMessage());
141 mBrokenLibraries = true;
142 errorMsgBrokenLib = e.getMessage();
145 if (mBrokenLibraries)
147 AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this);
148 dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall."
149 + System.getProperty("line.separator")
150 + System.getProperty("line.separator")
151 + "Error: " + errorMsgBrokenLib);
152 dlgAlert.setTitle("SDL Error");
153 dlgAlert.setPositiveButton("Exit",
154 new DialogInterface.OnClickListener() {
156 public void onClick(DialogInterface dialog,int id) {
157 // if this button is clicked, close current activity
158 SDLActivity.mSingleton.finish();
161 dlgAlert.setCancelable(false);
162 dlgAlert.create().show();
167 // Set up the surface
168 mSurface = new SDLSurface(getApplication());
170 if(Build.VERSION.SDK_INT >= 12) {
171 mJoystickHandler = new SDLJoystickHandler_API12();
174 mJoystickHandler = new SDLJoystickHandler();
177 mLayout = new RelativeLayout(this);
178 mLayout.addView(mSurface);
180 setContentView(mLayout);
182 // Get filename from "Open with" of another application
183 Intent intent = getIntent();
185 if (intent != null && intent.getData() != null) {
186 String filename = intent.getData().getPath();
187 if (filename != null) {
188 Log.v(TAG, "Got filename: " + filename);
189 SDLActivity.onNativeDropFile(filename);
196 protected void onPause() {
197 Log.v(TAG, "onPause()");
200 if (SDLActivity.mBrokenLibraries) {
204 SDLActivity.handlePause();
208 protected void onResume() {
209 Log.v(TAG, "onResume()");
212 if (SDLActivity.mBrokenLibraries) {
216 SDLActivity.handleResume();
221 public void onWindowFocusChanged(boolean hasFocus) {
222 super.onWindowFocusChanged(hasFocus);
223 Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
225 if (SDLActivity.mBrokenLibraries) {
229 SDLActivity.mHasFocus = hasFocus;
231 SDLActivity.handleResume();
236 public void onLowMemory() {
237 Log.v(TAG, "onLowMemory()");
240 if (SDLActivity.mBrokenLibraries) {
244 SDLActivity.nativeLowMemory();
248 protected void onDestroy() {
249 Log.v(TAG, "onDestroy()");
251 if (SDLActivity.mBrokenLibraries) {
253 // Reset everything in case the user re opens the app
254 SDLActivity.initialize();
258 // Send a quit message to the application
259 SDLActivity.mExitCalledFromJava = true;
260 SDLActivity.nativeQuit();
262 // Now wait for the SDL thread to quit
263 if (SDLActivity.mSDLThread != null) {
265 SDLActivity.mSDLThread.join();
266 } catch(Exception e) {
267 Log.v(TAG, "Problem stopping thread: " + e);
269 SDLActivity.mSDLThread = null;
271 //Log.v(TAG, "Finished waiting for SDL thread");
275 // Reset everything in case the user re opens the app
276 SDLActivity.initialize();
280 public boolean dispatchKeyEvent(KeyEvent event) {
282 if (SDLActivity.mBrokenLibraries) {
286 int keyCode = event.getKeyCode();
287 // Ignore certain special keys so they're handled by Android
288 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
289 keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
290 keyCode == KeyEvent.KEYCODE_CAMERA ||
291 keyCode == 168 || /* API 11: KeyEvent.KEYCODE_ZOOM_IN */
292 keyCode == 169 /* API 11: KeyEvent.KEYCODE_ZOOM_OUT */
296 return super.dispatchKeyEvent(event);
299 /** Called by onPause or surfaceDestroyed. Even if surfaceDestroyed
300 * is the first to be called, mIsSurfaceReady should still be set
301 * to 'true' during the call to onPause (in a usual scenario).
303 public static void handlePause() {
304 if (!SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady) {
305 SDLActivity.mIsPaused = true;
306 SDLActivity.nativePause();
307 mSurface.handlePause();
311 /** Called by onResume or surfaceCreated. An actual resume should be done only when the surface is ready.
312 * Note: Some Android variants may send multiple surfaceChanged events, so we don't need to resume
313 * every time we get one of those events, only if it comes after surfaceDestroyed
315 public static void handleResume() {
316 if (SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady && SDLActivity.mHasFocus) {
317 SDLActivity.mIsPaused = false;
318 SDLActivity.nativeResume();
319 mSurface.handleResume();
323 /* The native thread has finished */
324 public static void handleNativeExit() {
325 SDLActivity.mSDLThread = null;
330 // Messages from the SDLMain thread
331 static final int COMMAND_CHANGE_TITLE = 1;
332 static final int COMMAND_UNUSED = 2;
333 static final int COMMAND_TEXTEDIT_HIDE = 3;
334 static final int COMMAND_SET_KEEP_SCREEN_ON = 5;
336 protected static final int COMMAND_USER = 0x8000;
339 * This method is called by SDL if SDL did not handle a message itself.
340 * This happens if a received message contains an unsupported command.
341 * Method can be overwritten to handle Messages in a different class.
342 * @param command the command of the message.
343 * @param param the parameter of the message. May be null.
344 * @return if the message was handled in overridden method.
346 protected boolean onUnhandledMessage(int command, Object param) {
351 * A Handler class for Messages from native SDL applications.
352 * It uses current Activities as target (e.g. for the title).
353 * static to prevent implicit references to enclosing object.
355 protected static class SDLCommandHandler extends Handler {
357 public void handleMessage(Message msg) {
358 Context context = getContext();
359 if (context == null) {
360 Log.e(TAG, "error handling message, getContext() returned null");
364 case COMMAND_CHANGE_TITLE:
365 if (context instanceof Activity) {
366 ((Activity) context).setTitle((String)msg.obj);
368 Log.e(TAG, "error handling message, getContext() returned no Activity");
371 case COMMAND_TEXTEDIT_HIDE:
372 if (mTextEdit != null) {
373 // Note: On some devices setting view to GONE creates a flicker in landscape.
374 // Setting the View's sizes to 0 is similar to GONE but without the flicker.
375 // The sizes will be set to useful values when the keyboard is shown again.
376 mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0));
378 InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
379 imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0);
382 case COMMAND_SET_KEEP_SCREEN_ON:
384 Window window = ((Activity) context).getWindow();
385 if (window != null) {
386 if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) {
387 window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
389 window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
395 if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) {
396 Log.e(TAG, "error handling message, command is " + msg.arg1);
402 // Handler for the messages
403 Handler commandHandler = new SDLCommandHandler();
405 // Send a message from the SDLMain thread
406 boolean sendCommand(int command, Object data) {
407 Message msg = commandHandler.obtainMessage();
410 return commandHandler.sendMessage(msg);
413 // C functions we call
414 public static native int nativeInit(Object arguments);
415 public static native void nativeLowMemory();
416 public static native void nativeQuit();
417 public static native void nativePause();
418 public static native void nativeResume();
419 public static native void onNativeDropFile(String filename);
420 public static native void onNativeResize(int x, int y, int format, float rate);
421 public static native int onNativePadDown(int device_id, int keycode);
422 public static native int onNativePadUp(int device_id, int keycode);
423 public static native void onNativeJoy(int device_id, int axis,
425 public static native void onNativeHat(int device_id, int hat_id,
427 public static native void onNativeKeyDown(int keycode);
428 public static native void onNativeKeyUp(int keycode);
429 public static native void onNativeKeyboardFocusLost();
430 public static native void onNativeMouse(int button, int action, float x, float y);
431 public static native void onNativeTouch(int touchDevId, int pointerFingerId,
434 public static native void onNativeAccel(float x, float y, float z);
435 public static native void onNativeSurfaceChanged();
436 public static native void onNativeSurfaceDestroyed();
437 public static native int nativeAddJoystick(int device_id, String name,
438 int is_accelerometer, int nbuttons,
439 int naxes, int nhats, int nballs);
440 public static native int nativeRemoveJoystick(int device_id);
441 public static native String nativeGetHint(String name);
444 * This method is called by SDL using JNI.
446 public static boolean setActivityTitle(String title) {
447 // Called from SDLMain() thread and can't directly affect the view
448 return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
452 * This method is called by SDL using JNI.
454 public static boolean sendMessage(int command, int param) {
455 return mSingleton.sendCommand(command, Integer.valueOf(param));
459 * This method is called by SDL using JNI.
461 public static Context getContext() {
466 * This method is called by SDL using JNI.
467 * @return result of getSystemService(name) but executed on UI thread.
469 public Object getSystemServiceFromUiThread(final String name) {
470 final Object lock = new Object();
471 final Object[] results = new Object[2]; // array for writable variables
472 synchronized (lock) {
473 runOnUiThread(new Runnable() {
476 synchronized (lock) {
477 results[0] = getSystemService(name);
478 results[1] = Boolean.TRUE;
483 if (results[1] == null) {
486 } catch (InterruptedException ex) {
487 ex.printStackTrace();
494 static class ShowTextInputTask implements Runnable {
496 * This is used to regulate the pan&scan method to have some offset from
497 * the bottom edge of the input region and the top edge of an input
498 * method (soft keyboard)
500 static final int HEIGHT_PADDING = 15;
502 public int x, y, w, h;
504 public ShowTextInputTask(int x, int y, int w, int h) {
513 RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING);
514 params.leftMargin = x;
515 params.topMargin = y;
517 if (mTextEdit == null) {
518 mTextEdit = new DummyEdit(getContext());
520 mLayout.addView(mTextEdit, params);
522 mTextEdit.setLayoutParams(params);
525 mTextEdit.setVisibility(View.VISIBLE);
526 mTextEdit.requestFocus();
528 InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
529 imm.showSoftInput(mTextEdit, 0);
534 * This method is called by SDL using JNI.
536 public static boolean showTextInput(int x, int y, int w, int h) {
537 // Transfer the task to the main thread as a Runnable
538 return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h));
542 * This method is called by SDL using JNI.
544 public static Surface getNativeSurface() {
545 return SDLActivity.mSurface.getNativeSurface();
551 * This method is called by SDL using JNI.
553 public static int audioOpen(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) {
554 int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO;
555 int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT;
556 int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1);
558 Log.v(TAG, "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer");
560 // Let the user pick a larger buffer if they really want -- but ye
561 // gods they probably shouldn't, the minimums are horrifyingly high
563 desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize);
565 if (mAudioTrack == null) {
566 mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
567 channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
569 // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
570 // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
571 // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
573 if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
574 Log.e(TAG, "Failed during initialization of Audio Track");
582 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");
588 * This method is called by SDL using JNI.
590 public static void audioWriteShortBuffer(short[] buffer) {
591 for (int i = 0; i < buffer.length; ) {
592 int result = mAudioTrack.write(buffer, i, buffer.length - i);
595 } else if (result == 0) {
598 } catch(InterruptedException e) {
602 Log.w(TAG, "SDL audio: error return from write(short)");
609 * This method is called by SDL using JNI.
611 public static void audioWriteByteBuffer(byte[] buffer) {
612 for (int i = 0; i < buffer.length; ) {
613 int result = mAudioTrack.write(buffer, i, buffer.length - i);
616 } else if (result == 0) {
619 } catch(InterruptedException e) {
623 Log.w(TAG, "SDL audio: error return from write(byte)");
630 * This method is called by SDL using JNI.
632 public static int captureOpen(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) {
633 int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO;
634 int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT;
635 int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1);
637 Log.v(TAG, "SDL capture: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer");
639 // Let the user pick a larger buffer if they really want -- but ye
640 // gods they probably shouldn't, the minimums are horrifyingly high
642 desiredFrames = Math.max(desiredFrames, (AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize);
644 if (mAudioRecord == null) {
645 mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
646 channelConfig, audioFormat, desiredFrames * frameSize);
648 // see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
649 if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
650 Log.e(TAG, "Failed during initialization of AudioRecord");
651 mAudioRecord.release();
656 mAudioRecord.startRecording();
659 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");
664 /** This method is called by SDL using JNI. */
665 public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
666 // !!! FIXME: this is available in API Level 23. Until then, we always block. :(
667 //return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
668 return mAudioRecord.read(buffer, 0, buffer.length);
671 /** This method is called by SDL using JNI. */
672 public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
673 // !!! FIXME: this is available in API Level 23. Until then, we always block. :(
674 //return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
675 return mAudioRecord.read(buffer, 0, buffer.length);
679 /** This method is called by SDL using JNI. */
680 public static void audioClose() {
681 if (mAudioTrack != null) {
683 mAudioTrack.release();
688 /** This method is called by SDL using JNI. */
689 public static void captureClose() {
690 if (mAudioRecord != null) {
692 mAudioRecord.release();
701 * This method is called by SDL using JNI.
702 * @return an array which may be empty but is never null.
704 public static int[] inputGetInputDeviceIds(int sources) {
705 int[] ids = InputDevice.getDeviceIds();
706 int[] filtered = new int[ids.length];
708 for (int i = 0; i < ids.length; ++i) {
709 InputDevice device = InputDevice.getDevice(ids[i]);
710 if ((device != null) && ((device.getSources() & sources) != 0)) {
711 filtered[used++] = device.getId();
714 return Arrays.copyOf(filtered, used);
717 // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
718 public static boolean handleJoystickMotionEvent(MotionEvent event) {
719 return mJoystickHandler.handleMotionEvent(event);
723 * This method is called by SDL using JNI.
725 public static void pollInputDevices() {
726 if (SDLActivity.mSDLThread != null) {
727 mJoystickHandler.pollInputDevices();
731 // Check if a given device is considered a possible SDL joystick
732 public static boolean isDeviceSDLJoystick(int deviceId) {
733 InputDevice device = InputDevice.getDevice(deviceId);
734 // We cannot use InputDevice.isVirtual before API 16, so let's accept
735 // only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
736 if ((device == null) || (deviceId < 0)) {
739 int sources = device.getSources();
740 return (((sources & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) ||
741 ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
742 ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
746 // APK expansion files support
748 /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */
749 private Object expansionFile;
751 /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */
752 private Method expansionFileMethod;
755 * This method is called by SDL using JNI.
756 * @return an InputStream on success or null if no expansion file was used.
757 * @throws IOException on errors. Message is set for the SDL error message.
759 public InputStream openAPKExpansionInputStream(String fileName) throws IOException {
760 // Get a ZipResourceFile representing a merger of both the main and patch files
761 if (expansionFile == null) {
762 String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION");
763 if (mainHint == null) {
764 return null; // no expansion use if no main version was set
766 String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION");
767 if (patchHint == null) {
768 return null; // no expansion use if no patch version was set
772 Integer patchVersion;
774 mainVersion = Integer.valueOf(mainHint);
775 patchVersion = Integer.valueOf(patchHint);
776 } catch (NumberFormatException ex) {
777 ex.printStackTrace();
778 throw new IOException("No valid file versions set for APK expansion files", ex);
782 // To avoid direct dependency on Google APK expansion library that is
783 // not a part of Android SDK we access it using reflection
784 expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport")
785 .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class)
786 .invoke(null, this, mainVersion, patchVersion);
788 expansionFileMethod = expansionFile.getClass()
789 .getMethod("getInputStream", String.class);
790 } catch (Exception ex) {
791 ex.printStackTrace();
792 expansionFile = null;
793 expansionFileMethod = null;
794 throw new IOException("Could not access APK expansion support library", ex);
798 // Get an input stream for a known file inside the expansion file ZIPs
799 InputStream fileStream;
801 fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName);
802 } catch (Exception ex) {
803 // calling "getInputStream" failed
804 ex.printStackTrace();
805 throw new IOException("Could not open stream from APK expansion file", ex);
808 if (fileStream == null) {
809 // calling "getInputStream" was successful but null was returned
810 throw new IOException("Could not find path in APK expansion file");
818 /** Result of current messagebox. Also used for blocking the calling thread. */
819 protected final int[] messageboxSelection = new int[1];
821 /** Id of current dialog. */
822 protected int dialogs = 0;
825 * This method is called by SDL using JNI.
826 * Shows the messagebox from UI thread and block calling thread.
827 * buttonFlags, buttonIds and buttonTexts must have same length.
828 * @param buttonFlags array containing flags for every button.
829 * @param buttonIds array containing id for every button.
830 * @param buttonTexts array containing text for every button.
831 * @param colors null for default or array of length 5 containing colors.
832 * @return button id or -1.
834 public int messageboxShowMessageBox(
837 final String message,
838 final int[] buttonFlags,
839 final int[] buttonIds,
840 final String[] buttonTexts,
841 final int[] colors) {
843 messageboxSelection[0] = -1;
847 if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) {
848 return -1; // implementation broken
851 // collect arguments for Dialog
853 final Bundle args = new Bundle();
854 args.putInt("flags", flags);
855 args.putString("title", title);
856 args.putString("message", message);
857 args.putIntArray("buttonFlags", buttonFlags);
858 args.putIntArray("buttonIds", buttonIds);
859 args.putStringArray("buttonTexts", buttonTexts);
860 args.putIntArray("colors", colors);
862 // trigger Dialog creation on UI thread
864 runOnUiThread(new Runnable() {
867 showDialog(dialogs++, args);
871 // block the calling thread
873 synchronized (messageboxSelection) {
875 messageboxSelection.wait();
876 } catch (InterruptedException ex) {
877 ex.printStackTrace();
882 // return selected value
884 return messageboxSelection[0];
888 protected Dialog onCreateDialog(int ignore, Bundle args) {
890 // TODO set values from "flags" to messagebox dialog
894 int[] colors = args.getIntArray("colors");
897 int buttonBorderColor;
898 int buttonBackgroundColor;
899 int buttonSelectedColor;
900 if (colors != null) {
902 backgroundColor = colors[++i];
903 textColor = colors[++i];
904 buttonBorderColor = colors[++i];
905 buttonBackgroundColor = colors[++i];
906 buttonSelectedColor = colors[++i];
908 backgroundColor = Color.TRANSPARENT;
909 textColor = Color.TRANSPARENT;
910 buttonBorderColor = Color.TRANSPARENT;
911 buttonBackgroundColor = Color.TRANSPARENT;
912 buttonSelectedColor = Color.TRANSPARENT;
915 // create dialog with title and a listener to wake up calling thread
917 final Dialog dialog = new Dialog(this);
918 dialog.setTitle(args.getString("title"));
919 dialog.setCancelable(false);
920 dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
922 public void onDismiss(DialogInterface unused) {
923 synchronized (messageboxSelection) {
924 messageboxSelection.notify();
931 TextView message = new TextView(this);
932 message.setGravity(Gravity.CENTER);
933 message.setText(args.getString("message"));
934 if (textColor != Color.TRANSPARENT) {
935 message.setTextColor(textColor);
940 int[] buttonFlags = args.getIntArray("buttonFlags");
941 int[] buttonIds = args.getIntArray("buttonIds");
942 String[] buttonTexts = args.getStringArray("buttonTexts");
944 final SparseArray<Button> mapping = new SparseArray<Button>();
946 LinearLayout buttons = new LinearLayout(this);
947 buttons.setOrientation(LinearLayout.HORIZONTAL);
948 buttons.setGravity(Gravity.CENTER);
949 for (int i = 0; i < buttonTexts.length; ++i) {
950 Button button = new Button(this);
951 final int id = buttonIds[i];
952 button.setOnClickListener(new View.OnClickListener() {
954 public void onClick(View v) {
955 messageboxSelection[0] = id;
959 if (buttonFlags[i] != 0) {
960 // see SDL_messagebox.h
961 if ((buttonFlags[i] & 0x00000001) != 0) {
962 mapping.put(KeyEvent.KEYCODE_ENTER, button);
964 if ((buttonFlags[i] & 0x00000002) != 0) {
965 mapping.put(111, button); /* API 11: KeyEvent.KEYCODE_ESCAPE */
968 button.setText(buttonTexts[i]);
969 if (textColor != Color.TRANSPARENT) {
970 button.setTextColor(textColor);
972 if (buttonBorderColor != Color.TRANSPARENT) {
973 // TODO set color for border of messagebox button
975 if (buttonBackgroundColor != Color.TRANSPARENT) {
976 Drawable drawable = button.getBackground();
977 if (drawable == null) {
978 // setting the color this way removes the style
979 button.setBackgroundColor(buttonBackgroundColor);
981 // setting the color this way keeps the style (gradient, padding, etc.)
982 drawable.setColorFilter(buttonBackgroundColor, PorterDuff.Mode.MULTIPLY);
985 if (buttonSelectedColor != Color.TRANSPARENT) {
986 // TODO set color for selected messagebox button
988 buttons.addView(button);
993 LinearLayout content = new LinearLayout(this);
994 content.setOrientation(LinearLayout.VERTICAL);
995 content.addView(message);
996 content.addView(buttons);
997 if (backgroundColor != Color.TRANSPARENT) {
998 content.setBackgroundColor(backgroundColor);
1001 // add content to dialog and return
1003 dialog.setContentView(content);
1004 dialog.setOnKeyListener(new Dialog.OnKeyListener() {
1006 public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) {
1007 Button button = mapping.get(keyCode);
1008 if (button != null) {
1009 if (event.getAction() == KeyEvent.ACTION_UP) {
1010 button.performClick();
1012 return true; // also for ignored actions
1023 Simple nativeInit() runnable
1025 class SDLMain implements Runnable {
1029 SDLActivity.nativeInit(SDLActivity.mSingleton.getArguments());
1031 //Log.v("SDL", "SDL thread terminated");
1037 SDLSurface. This is what we draw on, so we need to know when it's created
1038 in order to do anything useful.
1040 Because of this, that's where we set up the SDL thread
1042 class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
1043 View.OnKeyListener, View.OnTouchListener, SensorEventListener {
1046 protected static SensorManager mSensorManager;
1047 protected static Display mDisplay;
1049 // Keep track of the surface size to normalize touch events
1050 protected static float mWidth, mHeight;
1053 public SDLSurface(Context context) {
1055 getHolder().addCallback(this);
1058 setFocusableInTouchMode(true);
1060 setOnKeyListener(this);
1061 setOnTouchListener(this);
1063 mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
1064 mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
1066 if(Build.VERSION.SDK_INT >= 12) {
1067 setOnGenericMotionListener(new SDLGenericMotionListener_API12());
1070 // Some arbitrary defaults to avoid a potential division by zero
1075 public void handlePause() {
1076 enableSensor(Sensor.TYPE_ACCELEROMETER, false);
1079 public void handleResume() {
1081 setFocusableInTouchMode(true);
1083 setOnKeyListener(this);
1084 setOnTouchListener(this);
1085 enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1088 public Surface getNativeSurface() {
1089 return getHolder().getSurface();
1092 // Called when we have a valid drawing surface
1094 public void surfaceCreated(SurfaceHolder holder) {
1095 Log.v("SDL", "surfaceCreated()");
1096 holder.setType(SurfaceHolder.SURFACE_TYPE_GPU);
1099 // Called when we lose the surface
1101 public void surfaceDestroyed(SurfaceHolder holder) {
1102 Log.v("SDL", "surfaceDestroyed()");
1103 // Call this *before* setting mIsSurfaceReady to 'false'
1104 SDLActivity.handlePause();
1105 SDLActivity.mIsSurfaceReady = false;
1106 SDLActivity.onNativeSurfaceDestroyed();
1109 // Called when the surface is resized
1111 public void surfaceChanged(SurfaceHolder holder,
1112 int format, int width, int height) {
1113 Log.v("SDL", "surfaceChanged()");
1115 int sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 by default
1117 case PixelFormat.A_8:
1118 Log.v("SDL", "pixel format A_8");
1120 case PixelFormat.LA_88:
1121 Log.v("SDL", "pixel format LA_88");
1123 case PixelFormat.L_8:
1124 Log.v("SDL", "pixel format L_8");
1126 case PixelFormat.RGBA_4444:
1127 Log.v("SDL", "pixel format RGBA_4444");
1128 sdlFormat = 0x15421002; // SDL_PIXELFORMAT_RGBA4444
1130 case PixelFormat.RGBA_5551:
1131 Log.v("SDL", "pixel format RGBA_5551");
1132 sdlFormat = 0x15441002; // SDL_PIXELFORMAT_RGBA5551
1134 case PixelFormat.RGBA_8888:
1135 Log.v("SDL", "pixel format RGBA_8888");
1136 sdlFormat = 0x16462004; // SDL_PIXELFORMAT_RGBA8888
1138 case PixelFormat.RGBX_8888:
1139 Log.v("SDL", "pixel format RGBX_8888");
1140 sdlFormat = 0x16261804; // SDL_PIXELFORMAT_RGBX8888
1142 case PixelFormat.RGB_332:
1143 Log.v("SDL", "pixel format RGB_332");
1144 sdlFormat = 0x14110801; // SDL_PIXELFORMAT_RGB332
1146 case PixelFormat.RGB_565:
1147 Log.v("SDL", "pixel format RGB_565");
1148 sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565
1150 case PixelFormat.RGB_888:
1151 Log.v("SDL", "pixel format RGB_888");
1152 // Not sure this is right, maybe SDL_PIXELFORMAT_RGB24 instead?
1153 sdlFormat = 0x16161804; // SDL_PIXELFORMAT_RGB888
1156 Log.v("SDL", "pixel format unknown " + format);
1162 SDLActivity.onNativeResize(width, height, sdlFormat, mDisplay.getRefreshRate());
1163 Log.v("SDL", "Window size: " + width + "x" + height);
1166 boolean skip = false;
1167 int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
1169 if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
1173 else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
1175 if (mWidth > mHeight) {
1178 } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
1179 if (mWidth < mHeight) {
1184 // Special Patch for Square Resolution: Black Berry Passport
1186 double min = Math.min(mWidth, mHeight);
1187 double max = Math.max(mWidth, mHeight);
1189 if (max / min < 1.20) {
1190 Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
1196 Log.v("SDL", "Skip .. Surface is not ready.");
1201 // Set mIsSurfaceReady to 'true' *before* making a call to handleResume
1202 SDLActivity.mIsSurfaceReady = true;
1203 SDLActivity.onNativeSurfaceChanged();
1206 if (SDLActivity.mSDLThread == null) {
1207 // This is the entry point to the C app.
1208 // Start up the C app thread and enable sensor input for the first time
1210 final Thread sdlThread = new Thread(new SDLMain(), "SDLThread");
1211 enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1214 // Set up a listener thread to catch when the native thread ends
1215 SDLActivity.mSDLThread = new Thread(new Runnable(){
1221 catch(Exception e){}
1223 // Native thread has finished
1224 if (! SDLActivity.mExitCalledFromJava) {
1225 SDLActivity.handleNativeExit();
1229 }, "SDLThreadListener");
1230 SDLActivity.mSDLThread.start();
1233 if (SDLActivity.mHasFocus) {
1234 SDLActivity.handleResume();
1240 public boolean onKey(View v, int keyCode, KeyEvent event) {
1241 // Dispatch the different events depending on where they come from
1242 // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD
1243 // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD
1245 // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and
1246 // SOURCE_JOYSTICK, while its key events arrive from the keyboard source
1247 // So, retrieve the device itself and check all of its sources
1248 if (SDLActivity.isDeviceSDLJoystick(event.getDeviceId())) {
1249 // Note that we process events with specific key codes here
1250 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1251 if (SDLActivity.onNativePadDown(event.getDeviceId(), keyCode) == 0) {
1254 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1255 if (SDLActivity.onNativePadUp(event.getDeviceId(), keyCode) == 0) {
1261 if ((event.getSource() & InputDevice.SOURCE_KEYBOARD) != 0) {
1262 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1263 //Log.v("SDL", "key down: " + keyCode);
1264 SDLActivity.onNativeKeyDown(keyCode);
1267 else if (event.getAction() == KeyEvent.ACTION_UP) {
1268 //Log.v("SDL", "key up: " + keyCode);
1269 SDLActivity.onNativeKeyUp(keyCode);
1274 if ((event.getSource() & InputDevice.SOURCE_MOUSE) != 0) {
1275 // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses
1276 // they are ignored here because sending them as mouse input to SDL is messy
1277 if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) {
1278 switch (event.getAction()) {
1279 case KeyEvent.ACTION_DOWN:
1280 case KeyEvent.ACTION_UP:
1281 // mark the event as handled or it will be handled by system
1282 // handling KEYCODE_BACK by system will call onBackPressed()
1293 public boolean onTouch(View v, MotionEvent event) {
1294 /* Ref: http://developer.android.com/training/gestures/multi.html */
1295 final int touchDevId = event.getDeviceId();
1296 final int pointerCount = event.getPointerCount();
1297 int action = event.getActionMasked();
1298 int pointerFingerId;
1303 // !!! FIXME: dump this SDK check after 2.0.4 ships and require API14.
1304 if (event.getSource() == InputDevice.SOURCE_MOUSE && SDLActivity.mSeparateMouseAndTouch) {
1305 if (Build.VERSION.SDK_INT < 14) {
1306 mouseButton = 1; // all mouse buttons are the left button
1309 mouseButton = (Integer) event.getClass().getMethod("getButtonState").invoke(event);
1310 } catch(Exception e) {
1311 mouseButton = 1; // oh well.
1314 SDLActivity.onNativeMouse(mouseButton, action, event.getX(0), event.getY(0));
1317 case MotionEvent.ACTION_MOVE:
1318 for (i = 0; i < pointerCount; i++) {
1319 pointerFingerId = event.getPointerId(i);
1320 x = event.getX(i) / mWidth;
1321 y = event.getY(i) / mHeight;
1322 p = event.getPressure(i);
1324 // may be larger than 1.0f on some devices
1325 // see the documentation of getPressure(i)
1328 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1332 case MotionEvent.ACTION_UP:
1333 case MotionEvent.ACTION_DOWN:
1334 // Primary pointer up/down, the index is always zero
1336 case MotionEvent.ACTION_POINTER_UP:
1337 case MotionEvent.ACTION_POINTER_DOWN:
1338 // Non primary pointer up/down
1340 i = event.getActionIndex();
1343 pointerFingerId = event.getPointerId(i);
1344 x = event.getX(i) / mWidth;
1345 y = event.getY(i) / mHeight;
1346 p = event.getPressure(i);
1348 // may be larger than 1.0f on some devices
1349 // see the documentation of getPressure(i)
1352 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1355 case MotionEvent.ACTION_CANCEL:
1356 for (i = 0; i < pointerCount; i++) {
1357 pointerFingerId = event.getPointerId(i);
1358 x = event.getX(i) / mWidth;
1359 y = event.getY(i) / mHeight;
1360 p = event.getPressure(i);
1362 // may be larger than 1.0f on some devices
1363 // see the documentation of getPressure(i)
1366 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
1379 public void enableSensor(int sensortype, boolean enabled) {
1380 // TODO: This uses getDefaultSensor - what if we have >1 accels?
1382 mSensorManager.registerListener(this,
1383 mSensorManager.getDefaultSensor(sensortype),
1384 SensorManager.SENSOR_DELAY_GAME, null);
1386 mSensorManager.unregisterListener(this,
1387 mSensorManager.getDefaultSensor(sensortype));
1392 public void onAccuracyChanged(Sensor sensor, int accuracy) {
1397 public void onSensorChanged(SensorEvent event) {
1398 if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
1400 switch (mDisplay.getRotation()) {
1401 case Surface.ROTATION_90:
1402 x = -event.values[1];
1403 y = event.values[0];
1405 case Surface.ROTATION_270:
1406 x = event.values[1];
1407 y = -event.values[0];
1409 case Surface.ROTATION_180:
1410 x = -event.values[1];
1411 y = -event.values[0];
1414 x = event.values[0];
1415 y = event.values[1];
1418 SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
1419 y / SensorManager.GRAVITY_EARTH,
1420 event.values[2] / SensorManager.GRAVITY_EARTH);
1425 /* This is a fake invisible editor view that receives the input and defines the
1428 class DummyEdit extends View implements View.OnKeyListener {
1431 public DummyEdit(Context context) {
1433 setFocusableInTouchMode(true);
1435 setOnKeyListener(this);
1439 public boolean onCheckIsTextEditor() {
1444 public boolean onKey(View v, int keyCode, KeyEvent event) {
1446 // This handles the hardware keyboard input
1447 if (event.isPrintingKey() || keyCode == KeyEvent.KEYCODE_SPACE) {
1448 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1449 ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
1454 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1455 SDLActivity.onNativeKeyDown(keyCode);
1457 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1458 SDLActivity.onNativeKeyUp(keyCode);
1467 public boolean onKeyPreIme (int keyCode, KeyEvent event) {
1468 // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
1469 // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
1470 // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
1471 // FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
1472 // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
1473 // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
1474 if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
1475 if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
1476 SDLActivity.onNativeKeyboardFocusLost();
1479 return super.onKeyPreIme(keyCode, event);
1483 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
1484 ic = new SDLInputConnection(this, true);
1486 outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
1487 outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
1488 | 33554432 /* API 11: EditorInfo.IME_FLAG_NO_FULLSCREEN */;
1494 class SDLInputConnection extends BaseInputConnection {
1496 public SDLInputConnection(View targetView, boolean fullEditor) {
1497 super(targetView, fullEditor);
1502 public boolean sendKeyEvent(KeyEvent event) {
1505 * This handles the keycodes from soft keyboard (and IME-translated
1506 * input from hardkeyboard)
1508 int keyCode = event.getKeyCode();
1509 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1510 if (event.isPrintingKey() || keyCode == KeyEvent.KEYCODE_SPACE) {
1511 commitText(String.valueOf((char) event.getUnicodeChar()), 1);
1513 SDLActivity.onNativeKeyDown(keyCode);
1515 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1517 SDLActivity.onNativeKeyUp(keyCode);
1520 return super.sendKeyEvent(event);
1524 public boolean commitText(CharSequence text, int newCursorPosition) {
1526 nativeCommitText(text.toString(), newCursorPosition);
1528 return super.commitText(text, newCursorPosition);
1532 public boolean setComposingText(CharSequence text, int newCursorPosition) {
1534 nativeSetComposingText(text.toString(), newCursorPosition);
1536 return super.setComposingText(text, newCursorPosition);
1539 public native void nativeCommitText(String text, int newCursorPosition);
1541 public native void nativeSetComposingText(String text, int newCursorPosition);
1544 public boolean deleteSurroundingText(int beforeLength, int afterLength) {
1545 // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection
1546 if (beforeLength == 1 && afterLength == 0) {
1548 return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
1549 && super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
1552 return super.deleteSurroundingText(beforeLength, afterLength);
1556 /* A null joystick handler for API level < 12 devices (the accelerometer is handled separately) */
1557 class SDLJoystickHandler {
1560 * Handles given MotionEvent.
1561 * @param event the event to be handled.
1562 * @return if given event was processed.
1564 public boolean handleMotionEvent(MotionEvent event) {
1569 * Handles adding and removing of input devices.
1571 public void pollInputDevices() {
1575 /* Actual joystick functionality available for API >= 12 devices */
1576 class SDLJoystickHandler_API12 extends SDLJoystickHandler {
1578 static class SDLJoystick {
1579 public int device_id;
1581 public ArrayList<InputDevice.MotionRange> axes;
1582 public ArrayList<InputDevice.MotionRange> hats;
1584 static class RangeComparator implements Comparator<InputDevice.MotionRange> {
1586 public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
1587 return arg0.getAxis() - arg1.getAxis();
1591 private ArrayList<SDLJoystick> mJoysticks;
1593 public SDLJoystickHandler_API12() {
1595 mJoysticks = new ArrayList<SDLJoystick>();
1599 public void pollInputDevices() {
1600 int[] deviceIds = InputDevice.getDeviceIds();
1601 // It helps processing the device ids in reverse order
1602 // For example, in the case of the XBox 360 wireless dongle,
1603 // so the first controller seen by SDL matches what the receiver
1604 // considers to be the first controller
1606 for(int i=deviceIds.length-1; i>-1; i--) {
1607 SDLJoystick joystick = getJoystick(deviceIds[i]);
1608 if (joystick == null) {
1609 joystick = new SDLJoystick();
1610 InputDevice joystickDevice = InputDevice.getDevice(deviceIds[i]);
1611 if (SDLActivity.isDeviceSDLJoystick(deviceIds[i])) {
1612 joystick.device_id = deviceIds[i];
1613 joystick.name = joystickDevice.getName();
1614 joystick.axes = new ArrayList<InputDevice.MotionRange>();
1615 joystick.hats = new ArrayList<InputDevice.MotionRange>();
1617 List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
1618 Collections.sort(ranges, new RangeComparator());
1619 for (InputDevice.MotionRange range : ranges ) {
1620 if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
1621 if (range.getAxis() == MotionEvent.AXIS_HAT_X ||
1622 range.getAxis() == MotionEvent.AXIS_HAT_Y) {
1623 joystick.hats.add(range);
1626 joystick.axes.add(range);
1631 mJoysticks.add(joystick);
1632 SDLActivity.nativeAddJoystick(joystick.device_id, joystick.name, 0, -1,
1633 joystick.axes.size(), joystick.hats.size()/2, 0);
1638 /* Check removed devices */
1639 ArrayList<Integer> removedDevices = new ArrayList<Integer>();
1640 for(int i=0; i < mJoysticks.size(); i++) {
1641 int device_id = mJoysticks.get(i).device_id;
1643 for (j=0; j < deviceIds.length; j++) {
1644 if (device_id == deviceIds[j]) break;
1646 if (j == deviceIds.length) {
1647 removedDevices.add(Integer.valueOf(device_id));
1651 for(int i=0; i < removedDevices.size(); i++) {
1652 int device_id = removedDevices.get(i).intValue();
1653 SDLActivity.nativeRemoveJoystick(device_id);
1654 for (int j=0; j < mJoysticks.size(); j++) {
1655 if (mJoysticks.get(j).device_id == device_id) {
1656 mJoysticks.remove(j);
1663 protected SDLJoystick getJoystick(int device_id) {
1664 for(int i=0; i < mJoysticks.size(); i++) {
1665 if (mJoysticks.get(i).device_id == device_id) {
1666 return mJoysticks.get(i);
1673 public boolean handleMotionEvent(MotionEvent event) {
1674 if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0) {
1675 int actionPointerIndex = event.getActionIndex();
1676 int action = event.getActionMasked();
1678 case MotionEvent.ACTION_MOVE:
1679 SDLJoystick joystick = getJoystick(event.getDeviceId());
1680 if ( joystick != null ) {
1681 for (int i = 0; i < joystick.axes.size(); i++) {
1682 InputDevice.MotionRange range = joystick.axes.get(i);
1683 /* Normalize the value to -1...1 */
1684 float value = ( event.getAxisValue( range.getAxis(), actionPointerIndex) - range.getMin() ) / range.getRange() * 2.0f - 1.0f;
1685 SDLActivity.onNativeJoy(joystick.device_id, i, value );
1687 for (int i = 0; i < joystick.hats.size(); i+=2) {
1688 int hatX = Math.round(event.getAxisValue( joystick.hats.get(i).getAxis(), actionPointerIndex ) );
1689 int hatY = Math.round(event.getAxisValue( joystick.hats.get(i+1).getAxis(), actionPointerIndex ) );
1690 SDLActivity.onNativeHat(joystick.device_id, i/2, hatX, hatY );
1702 class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
1703 // Generic Motion (mouse hover, joystick...) events go here
1705 public boolean onGenericMotion(View v, MotionEvent event) {
1709 switch ( event.getSource() ) {
1710 case InputDevice.SOURCE_JOYSTICK:
1711 case InputDevice.SOURCE_GAMEPAD:
1712 case InputDevice.SOURCE_DPAD:
1713 return SDLActivity.handleJoystickMotionEvent(event);
1715 case InputDevice.SOURCE_MOUSE:
1716 action = event.getActionMasked();
1718 case MotionEvent.ACTION_SCROLL:
1719 x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
1720 y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
1721 SDLActivity.onNativeMouse(0, action, x, y);
1724 case MotionEvent.ACTION_HOVER_MOVE:
1728 SDLActivity.onNativeMouse(0, action, x, y);
1740 // Event was not managed